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

CoverRyan pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/daffodil-vscode.git


The following commit(s) were added to refs/heads/main by this push:
     new 2877674e add feature to prevent invalid tunables
2877674e is described below

commit 2877674efa8debc92ffd2d6595e2f106b685558e
Author: Adedoyin Ogunjobi <[email protected]>
AuthorDate: Wed Jun 24 14:43:49 2026 -0400

    add feature to prevent invalid tunables
    
    changes to scripts.js
    
    dont let users save invalud key or value
    
    move tunables.json and add feature
    
    Document allowed tunables and error handling
    
    Added section on allowed tunables and their validation process.
    
    Exclude tunables.json from RAT and prettier
    
    documentation + more tunables + verifiy values
    
    fix error not showing up. only the first error will show
    
    make invalids red
    
    proper fix for rendering error
    
    Make error show for each invalid tunable
    
    add fetch retry logic
    
    Update sbt-scalafmt to 2.6.1
    
    Update scalafmt-core to 3.11.1
---
 constants/tunables.json              |  76 ++++++++++++++++++++
 doc/Wiki.md                          |  11 +++
 project/Rat.scala                    |   1 +
 src/adapter/activateDaffodilDebug.ts |  62 +++++++++++++++-
 src/adapter/extension.ts             |  13 ++++
 src/launchWizard/launchWizard.ts     |  24 +++++++
 src/launchWizard/script.js           | 134 ++++++++++++++++++++++++++++++++---
 7 files changed, 309 insertions(+), 12 deletions(-)

diff --git a/constants/tunables.json b/constants/tunables.json
new file mode 100644
index 00000000..2b85a4d7
--- /dev/null
+++ b/constants/tunables.json
@@ -0,0 +1,76 @@
+{
+  "allowExpressionResultCoercion": "boolean",
+  "allowExternalPathExpressions": "boolean",
+  "allowSignedIntegerLength1Bit": "boolean",
+
+  "blobChunkSizeInBytes": "number",
+
+  "defaultEmptyElementParsePolicy": "string",
+
+  "escalateWarningsToErrors": "boolean",
+
+  "generatedNamespacePrefixStem": "string",
+
+  "initialElementOccurrencesHint": "number",
+  "initialRegexMatchLimitInCharacters": "number",
+
+  "infosetWalkerSkipMin": "number",
+  "infosetWalkerSkipMax": "number",
+
+  "invalidRestrictionPolicy": {
+    "type": "enum",
+    "values": ["error", "ignore", "validate"]
+  },
+
+  "maxBinaryDecimalVirtualPoint": "number",
+  "maxByteArrayOutputStreamBufferSizeInBytes": "number",
+  "maxDataDumpSizeInBytes": "number",
+  "maxHexBinaryLengthInBytes": "number",
+
+  "maxLengthForVariableLengthDelimiterDisplay": "number",
+  "maxLookaheadFunctionBits": "number",
+
+  "maxOccursBounds": "number",
+
+  "maxSkipLengthInBytes": "number",
+
+  "maxValidYear": "number",
+
+  "maximumRegexMatchLengthInCharacters": "number",
+  "maximumSimpleElementSizeInCharacters": "number",
+
+  "minBinaryDecimalVirtualPoint": "number",
+  "minValidYear": "number",
+
+  "outputStreamChunkSizeInBytes": "number",
+
+  "parseUnparsePolicy": "string",
+
+  "releaseUnneededInfoset": "boolean",
+
+  "requireBitOrderProperty": "boolean",
+  "requireEmptyElementParsePolicyProperty": "boolean",
+
+  "requireEncodingErrorPolicyProperty": "boolean",
+  "requireFloatingProperty": "boolean",
+  "requireTextBidiProperty": "boolean",
+  "requireTextStandardBaseProperty": "boolean",
+
+  "saxUnparseEventBatchSize": "number",
+
+  "suppressSchemaDefinitionWarnings": "string",
+
+  "tempFilePath": "string",
+
+  "unqualifiedPathStepPolicy": {
+    "type": "enum",
+    "values": [
+      "noNamespace",
+      "defaultNamespace",
+      "preferDefaultNamespace"
+    ]
+  },
+
+  "unparseSuspensionWaitOld": "number",
+  "unparseSuspensionWaitYoung": "number"
+}
diff --git a/doc/Wiki.md b/doc/Wiki.md
index 2d56f7c3..d4963dee 100644
--- a/doc/Wiki.md
+++ b/doc/Wiki.md
@@ -372,12 +372,23 @@ Tunables are configured using **Key** and **Value** 
fields in the launch configu
 
 Use the **“+ Add Tunable”** button to add additional entries. Each row can be 
removed using the **“X”** button.
 
+Tunables are case sensitive, and only allowed tunables will be allowed to be 
saved in the wizard.
+
 Tunables control runtime behavior and performance characteristics. Most 
tunables have default values, so you only need to specify a value if you want 
to override them.
 
 <img width="790" height="176" alt="tunables_gui" 
src="https://github.com/user-attachments/assets/f78d5471-bfff-4cfb-b6cf-62c787a8e31b";
 />
 
 <img width="360" height="177" alt="tunables_launchjson" 
src="https://github.com/user-attachments/assets/0d70044d-d04c-4f47-958c-175c6e67f3f9";
 />
 
+### List of allowed Tunables
+
+In order to prevent unexpected tunables from being ran, there is a list of 
allowed tunables at `constants/tunables.json` which contains all valid 
tunables. Tunables are case sensitive and the values type must match what is 
expected. For example boolean values will only allow true or false. If a 
invalid tunable is saved in the `launch.json` , when you attempt to run 
daffodil you will encounter a error saying `Cancel` or `Ignore invalid 
tunable`. Cancel will send you back where you fix the [...]
+
+The validation for tunable values is reasonably loose to allow for possibly 
unknown tunables to be applied. For example suppressSchemaDefinitionWarnings 
documentation didn't explicitly list all the possible values, but 
unqualifiedPathStepPolicy explicitly stated the allowed values so only those 
values are allowing.
+
+<img width="914" height="718" alt="image" 
src="https://github.com/user-attachments/assets/22b24b29-de7e-4ec9-b012-85fe8f1bb54c";
 />
+
+
 ### Reference
 - https://daffodil.apache.org/tunables/
 
diff --git a/project/Rat.scala b/project/Rat.scala
index e8b214e1..3fc9056c 100644
--- a/project/Rat.scala
+++ b/project/Rat.scala
@@ -52,6 +52,7 @@ object Rat {
     file("build/package/NOLICENSE"),
     file("build/package/NONOTICE"),
     file("src/tests/data/test.txt"),
+    file("constants/tunables.json"),
     file("debugger/src/test/data/emptyData.xml"),
     file("debugger/src/test/data/emptyInfoset.xml"),
     file("debugger/src/test/data/notInfoset.xml"),
diff --git a/src/adapter/activateDaffodilDebug.ts 
b/src/adapter/activateDaffodilDebug.ts
index 3f523def..dafdc92c 100644
--- a/src/adapter/activateDaffodilDebug.ts
+++ b/src/adapter/activateDaffodilDebug.ts
@@ -18,6 +18,7 @@ import * as rootCompletion from '../rootCompletion'
 import { tmpdir } from 'os'
 import JSZip from 'jszip'
 import { rm } from 'node:fs/promises'
+import { getTunables } from './extension'
 
 import {
   CancellationToken,
@@ -766,16 +767,15 @@ class DaffodilConfigurationProvider
   constructor(context: vscode.ExtensionContext) {
     this.context = context
   }
-
   /**
    * Massage a debug configuration just before a debug session is being 
launched,
    * e.g. add all missing attributes to the debug configuration.
    */
-  resolveDebugConfiguration(
+  async resolveDebugConfiguration(
     folder: WorkspaceFolder | undefined,
     config: DebugConfiguration,
     token?: CancellationToken
-  ): ProviderResult<DebugConfiguration> {
+  ): Promise<DebugConfiguration | undefined> {
     // if launch.json is missing or empty
     if (!config.type && !config.request && !config.name) {
       config = getConfig({ name: 'Launch', request: 'launch', type: 'dfdl' })
@@ -791,6 +791,62 @@ class DaffodilConfigurationProvider
       data: config.data || '${command:AskForDataName}',
     }
 
+    const validTunables = getTunables(this.context)
+    const currentTunables = config.tunables ?? {}
+
+    const invalidTunables = Object.keys(currentTunables).filter(
+      (name) => !(name in validTunables)
+    )
+
+    const invalidValues = Object.entries(currentTunables)
+      .filter(([name, value]) => {
+        if (!(name in validTunables)) {
+          return false
+        }
+
+        const type = validTunables[name]
+
+        switch (type) {
+          case 'boolean':
+            return value !== 'true' && value !== 'false'
+
+          case 'number':
+            return isNaN(Number(value))
+
+          case 'string':
+            return typeof value !== 'string'
+
+          default:
+            return false
+        }
+      })
+      .map(([name]) => name)
+
+    const invalid = [...invalidTunables, ...invalidValues]
+
+    if (invalid.length > 0) {
+      const messages = [
+        ...invalidTunables.map((name) => `${name} (invalid tunable)`),
+        ...invalidValues.map((name) => `${name} (invalid value)`),
+      ]
+
+      const choice = await vscode.window.showWarningMessage(
+        `Invalid tunables found:\n\n${messages.join('\n')}`,
+        { modal: true },
+        'Ignore Invalid Tunables'
+      )
+
+      if (choice !== 'Ignore Invalid Tunables') {
+        return undefined
+      }
+
+      config.tunables = { ...config.tunables }
+
+      for (const invalidTunable of invalid) {
+        delete config.tunables[invalidTunable]
+      }
+    }
+
     let dataFolder = config.data
 
     if (
diff --git a/src/adapter/extension.ts b/src/adapter/extension.ts
index 13418217..fd8aa172 100644
--- a/src/adapter/extension.ts
+++ b/src/adapter/extension.ts
@@ -19,6 +19,8 @@
 
 import * as vscode from 'vscode'
 import * as position from '../position'
+import * as fs from 'fs'
+import * as path from 'path'
 import { ProviderResult } from 'vscode'
 import { DaffodilDebugSession } from './daffodilDebug'
 import {
@@ -45,6 +47,17 @@ export function deactivate() {
   position.deactivate()
 }
 
+export function getTunables(
+  context: vscode.ExtensionContext
+): Record<string, string> {
+  const file = path.join(context.extensionPath, 'constants', 'tunables.json')
+
+  const data = fs.readFileSync(file, 'utf8')
+  const json = JSON.parse(data)
+
+  return json
+}
+
 export class InlineDebugAdapterFactory
   implements vscode.DebugAdapterDescriptorFactory
 {
diff --git a/src/launchWizard/launchWizard.ts b/src/launchWizard/launchWizard.ts
index c130a8fe..63ff2a19 100644
--- a/src/launchWizard/launchWizard.ts
+++ b/src/launchWizard/launchWizard.ts
@@ -22,6 +22,7 @@ import { DFDLDebugger } from '../classes/dfdlDebugger'
 import { VSCodeLaunchConfigArgs } from '../classes/vscode-launch'
 import { DataEditorConfig } from '../classes/dataEditor'
 import { parse as jsoncParse } from 'jsonc-parser'
+import * as path from 'path'
 
 let launchWizard: LaunchWizard | undefined
 
@@ -279,6 +280,20 @@ async function createWizard(ctx: vscode.ExtensionContext) {
   let panel = launchWiz.getPanel()
   panel.webview.html = launchWiz.getWebViewContent()
 
+  //Read in list of valid
+
+  const tunablesPath = path.join(
+    ctx.extensionPath,
+    'constants',
+    'tunables.json'
+  )
+
+  const tunables = JSON.parse(fs.readFileSync(tunablesPath, 'utf8'))
+  panel.webview.postMessage({
+    command: 'loadTunables',
+    tunables: tunables,
+  })
+
   panel.webview.onDidReceiveMessage(
     async (message) => {
       switch (message.command) {
@@ -777,11 +792,20 @@ class LaunchWizard {
         </button>
       </div>
 
+      <div id="tunableErrorContainer"
+      style="color:red;font-size:12px;">
+       </div>
+
       <div id="VariablesDiv" class="setting-div" style="margin-top: 15px;">
         <p>Variables:</p>
         <p class="setting-description">Key/value configuration options</p>
 
         <table style="width: 100%; border-collapse: collapse; margin-top: 
5px;">
+          <style>
+            td {
+              vertical-align: top;
+            }
+          </style>
           <thead>
             <tr>
               <th style="text-align: left;">Key</th>
diff --git a/src/launchWizard/script.js b/src/launchWizard/script.js
index 63811c55..366ae663 100644
--- a/src/launchWizard/script.js
+++ b/src/launchWizard/script.js
@@ -348,6 +348,19 @@ function save() {
       )
   }
 
+  // Blocks save if tunable isnt valid. This means we have to always make sure 
the tunable list is valid.
+  // should we just warn the user that it will case errors and let them submit
+  validateTunablesTable()
+
+  const hasErrors = [
+    ...document.querySelectorAll('#tunablesTableBody .tunable-error'),
+  ].some((el) => el.textContent.trim())
+
+  if (hasErrors) {
+    console.warn('Cannot save: invalid tunables present')
+    return
+  }
+
   vscode.postMessage({
     command: 'saveConfig',
     data: JSON.stringify(obj, null, 4),
@@ -361,14 +374,22 @@ function addTunableRow() {
   const row = document.createElement('tr')
 
   row.innerHTML = `
-    <td><input class="file-input" /></td>
-    <td><input class="file-input" /></td>
-    <td><button onclick="this.closest('tr').remove()">X</button></td>
+  <td style="position: relative; vertical-align: top;">
+    <input class="file-input" />
+    <div class="tunable-error" 
+         style="color:red;font-size:12px;position:absolute;top:100%;left:0;">
+    </div>
+  </td>
+  <td style="vertical-align: top;">
+    <input class="file-input" />
+  </td>
+  <td style="vertical-align: top;">
+    <button onclick="removeTunableRow(this)">X</button>
+  </td>
   `
 
   tableBody.appendChild(row)
 }
-
 function getTunablesFromTable() {
   const rows = document.querySelectorAll('#tunablesTableBody tr')
   const tunables = {}
@@ -394,6 +415,12 @@ function escapeHtml(str) {
     .replace(/'/g, '&#39;')
 }
 
+function removeTunableRow(btn) {
+  const row = btn.closest('tr')
+  row.remove()
+  validateTunablesTable()
+}
+
 // function to pull tunables from config and render them in the tunables 
table, if there are any
 function renderTunables(tunables = {}) {
   const tableBody = document.getElementById('tunablesTableBody')
@@ -402,11 +429,20 @@ function renderTunables(tunables = {}) {
   Object.entries(tunables).forEach(([key, value]) => {
     const row = document.createElement('tr')
 
-    row.innerHTML = `
-      <td><input class="file-input" value="${escapeHtml(key)}" /></td>
-      <td><input class="file-input" value="${escapeHtml(value)}" /></td>
-      <td><button onclick="this.closest('tr').remove()">X</button></td>
-    `
+    row.innerHTML = row.innerHTML = `
+    <td>
+      <input class="file-input" value="${escapeHtml(key)}" />
+      <div class="tunable-error" style="color:red;font-size:12px;"></div>
+    </td>
+  
+    <td>
+      <input class="file-input" value="${escapeHtml(value)}" />
+    </td>
+  
+    <td>
+      <button onclick="this.closest('tr').remove(); 
validateTunablesTable();">X</button>
+    </td>
+  `
 
     tableBody.appendChild(row)
   })
@@ -463,7 +499,77 @@ function renderVariables(variables = {}) {
     tableBody.appendChild(row)
   })
 }
+let VALID_TUNABLES = {}
+
+// Validates tunables. validates against list of tunables and the value 
expected
+function validateTunablesTable() {
+  const rows = document.querySelectorAll('#tunablesTableBody tr')
+  const errorContainer = document.getElementById('tunableErrorContainer')
+
+  errorContainer.innerText = ''
 
+  let hasError = false
+  const errors = []
+
+  for (const row of rows) {
+    const keyInput = row.children[0].querySelector('input')
+    const valueInput = row.children[1].querySelector('input')
+
+    if (!keyInput || !valueInput) continue
+
+    const key = keyInput.value.trim()
+    const value = valueInput.value.trim()
+
+    let error = ''
+
+    // Reset styles
+    keyInput.style.color = 'white'
+    valueInput.style.color = 'white'
+
+    // Validate key
+    if (key && !VALID_TUNABLES[key]) {
+      const match = Object.keys(VALID_TUNABLES).find(
+        (t) => t.toLowerCase() === key.toLowerCase()
+      )
+
+      keyInput.style.color = 'red'
+      valueInput.style.color = 'red'
+
+      error = match
+        ? `Invalid tunable "${key}". Did you mean "${match}"?`
+        : `Invalid tunable "${key}"`
+    }
+    // Validate value
+    else if (key && value) {
+      const expectedType = VALID_TUNABLES[key]
+
+      if (expectedType === 'boolean') {
+        if (value !== 'true' && value !== 'false') {
+          valueInput.style.color = 'red'
+          error = 'Value must be boolean (true/false)'
+        }
+      } else if (expectedType === 'number') {
+        if (isNaN(Number(value))) {
+          valueInput.style.color = 'red'
+          error = 'Value must be a number'
+        }
+      } else if (expectedType?.type === 'enum') {
+        if (!expectedType.values.includes(value)) {
+          valueInput.style.color = 'red'
+          error = `Value must be one of: ${expectedType.values.join(', ')}`
+        }
+      }
+    }
+
+    if (error) {
+      hasError = true
+      errors.push(error)
+    }
+  }
+
+  errorContainer.innerText = errors.join('\n')
+  return !hasError
+}
 // Function to copy selected config
 function copyConfig() {
   const configValues = getConfigValues()
@@ -586,6 +692,12 @@ async function updateConfigValues(config) {
 
   renderTunables(config.tunables || {})
   renderVariables(config.variables || {})
+  document
+    .getElementById('tunablesTableBody')
+    ?.addEventListener('blur', validateTunablesTable, true) // remove true and 
change blur to input if we want this to validate on each key
+
+  // catches any invalid tunables on load.
+  validateTunablesTable()
   updateInfosetOutputType()
   updateTDMLAction()
 
@@ -615,6 +727,10 @@ async function updateDaffodilDebugClasspath(message) {
     const message = event.data
 
     switch (message.command) {
+      case 'loadTunables':
+        VALID_TUNABLES = message.tunables
+        // validateTunablesTable()
+        break
       case 'updateConfValues':
         await updateConfigValues(message.configValues)
         break

Reply via email to