From 3017befeb7d0b7bc99ea47b482d980db213e670f Mon Sep 17 00:00:00 2001 From: Stefan Date: Thu, 24 Sep 2026 12:10:33 +0200 Subject: [PATCH] minor fixes --- bindings/nodejs/index.js | 104 +++++++++--------- bindings/nodejs/package.json | 14 ++- core/engine/src/policy/queries/dependency.rs | 31 ++++-- core/engine/src/workspace/editor.rs | 6 +- core/engine/src/workspace/graph/analysis.rs | 32 ++++-- core/engine/src/workspace/graph/cursor.rs | 6 +- core/engine/src/workspace/slot/scope.rs | 8 +- .../engine/tests/data/policy/completions.toml | 31 +++++- .../engine/tests/data/policy/diagnostics.toml | 93 ++++++++++++++++ .../policy/fixtures/completion_scope.json | 63 +++++++++++ .../fixtures/completion_scope_graph.json | 72 ++++++++++++ core/engine/tests/policy_toml.rs | 29 +++-- .../expression/src/intellisense/completion.rs | 4 +- core/expression/src/parser/ast.rs | 2 +- core/expression/src/slot/classify.rs | 19 +++- core/expression/src/slot/mod.rs | 5 +- core/expression/tests/analysis.rs | 9 +- .../tests/data/analysis/diagnostics.toml | 10 ++ core/expression/tests/data/slots.toml | 50 +++++++++ core/expression/tests/slots.rs | 21 +++- 20 files changed, 509 insertions(+), 100 deletions(-) create mode 100644 core/engine/tests/data/policy/fixtures/completion_scope.json create mode 100644 core/engine/tests/data/policy/fixtures/completion_scope_graph.json diff --git a/bindings/nodejs/index.js b/bindings/nodejs/index.js index bba6e20b..fd25551a 100644 --- a/bindings/nodejs/index.js +++ b/bindings/nodejs/index.js @@ -77,8 +77,8 @@ function requireNative() { try { const binding = require('@gorules/zen-engine-android-arm64') const bindingPackageVersion = require('@gorules/zen-engine-android-arm64/package.json').version - if (bindingPackageVersion !== '2.0.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.0.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.0.3-local.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.0.3-local.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -93,8 +93,8 @@ function requireNative() { try { const binding = require('@gorules/zen-engine-android-arm-eabi') const bindingPackageVersion = require('@gorules/zen-engine-android-arm-eabi/package.json').version - if (bindingPackageVersion !== '2.0.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.0.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.0.3-local.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.0.3-local.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -114,8 +114,8 @@ function requireNative() { try { const binding = require('@gorules/zen-engine-win32-x64-gnu') const bindingPackageVersion = require('@gorules/zen-engine-win32-x64-gnu/package.json').version - if (bindingPackageVersion !== '2.0.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.0.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.0.3-local.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.0.3-local.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -130,8 +130,8 @@ function requireNative() { try { const binding = require('@gorules/zen-engine-win32-x64-msvc') const bindingPackageVersion = require('@gorules/zen-engine-win32-x64-msvc/package.json').version - if (bindingPackageVersion !== '2.0.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.0.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.0.3-local.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.0.3-local.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -147,8 +147,8 @@ function requireNative() { try { const binding = require('@gorules/zen-engine-win32-ia32-msvc') const bindingPackageVersion = require('@gorules/zen-engine-win32-ia32-msvc/package.json').version - if (bindingPackageVersion !== '2.0.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.0.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.0.3-local.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.0.3-local.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -163,8 +163,8 @@ function requireNative() { try { const binding = require('@gorules/zen-engine-win32-arm64-msvc') const bindingPackageVersion = require('@gorules/zen-engine-win32-arm64-msvc/package.json').version - if (bindingPackageVersion !== '2.0.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.0.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.0.3-local.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.0.3-local.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -182,8 +182,8 @@ function requireNative() { try { const binding = require('@gorules/zen-engine-darwin-universal') const bindingPackageVersion = require('@gorules/zen-engine-darwin-universal/package.json').version - if (bindingPackageVersion !== '2.0.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.0.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.0.3-local.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.0.3-local.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -198,8 +198,8 @@ function requireNative() { try { const binding = require('@gorules/zen-engine-darwin-x64') const bindingPackageVersion = require('@gorules/zen-engine-darwin-x64/package.json').version - if (bindingPackageVersion !== '2.0.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.0.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.0.3-local.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.0.3-local.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -214,8 +214,8 @@ function requireNative() { try { const binding = require('@gorules/zen-engine-darwin-arm64') const bindingPackageVersion = require('@gorules/zen-engine-darwin-arm64/package.json').version - if (bindingPackageVersion !== '2.0.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.0.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.0.3-local.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.0.3-local.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -234,8 +234,8 @@ function requireNative() { try { const binding = require('@gorules/zen-engine-freebsd-x64') const bindingPackageVersion = require('@gorules/zen-engine-freebsd-x64/package.json').version - if (bindingPackageVersion !== '2.0.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.0.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.0.3-local.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.0.3-local.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -250,8 +250,8 @@ function requireNative() { try { const binding = require('@gorules/zen-engine-freebsd-arm64') const bindingPackageVersion = require('@gorules/zen-engine-freebsd-arm64/package.json').version - if (bindingPackageVersion !== '2.0.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.0.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.0.3-local.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.0.3-local.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -271,8 +271,8 @@ function requireNative() { try { const binding = require('@gorules/zen-engine-linux-x64-musl') const bindingPackageVersion = require('@gorules/zen-engine-linux-x64-musl/package.json').version - if (bindingPackageVersion !== '2.0.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.0.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.0.3-local.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.0.3-local.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -287,8 +287,8 @@ function requireNative() { try { const binding = require('@gorules/zen-engine-linux-x64-gnu') const bindingPackageVersion = require('@gorules/zen-engine-linux-x64-gnu/package.json').version - if (bindingPackageVersion !== '2.0.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.0.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.0.3-local.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.0.3-local.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -305,8 +305,8 @@ function requireNative() { try { const binding = require('@gorules/zen-engine-linux-arm64-musl') const bindingPackageVersion = require('@gorules/zen-engine-linux-arm64-musl/package.json').version - if (bindingPackageVersion !== '2.0.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.0.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.0.3-local.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.0.3-local.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -321,8 +321,8 @@ function requireNative() { try { const binding = require('@gorules/zen-engine-linux-arm64-gnu') const bindingPackageVersion = require('@gorules/zen-engine-linux-arm64-gnu/package.json').version - if (bindingPackageVersion !== '2.0.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.0.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.0.3-local.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.0.3-local.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -339,8 +339,8 @@ function requireNative() { try { const binding = require('@gorules/zen-engine-linux-arm-musleabihf') const bindingPackageVersion = require('@gorules/zen-engine-linux-arm-musleabihf/package.json').version - if (bindingPackageVersion !== '2.0.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.0.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.0.3-local.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.0.3-local.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -355,8 +355,8 @@ function requireNative() { try { const binding = require('@gorules/zen-engine-linux-arm-gnueabihf') const bindingPackageVersion = require('@gorules/zen-engine-linux-arm-gnueabihf/package.json').version - if (bindingPackageVersion !== '2.0.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.0.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.0.3-local.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.0.3-local.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -373,8 +373,8 @@ function requireNative() { try { const binding = require('@gorules/zen-engine-linux-loong64-musl') const bindingPackageVersion = require('@gorules/zen-engine-linux-loong64-musl/package.json').version - if (bindingPackageVersion !== '2.0.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.0.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.0.3-local.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.0.3-local.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -389,8 +389,8 @@ function requireNative() { try { const binding = require('@gorules/zen-engine-linux-loong64-gnu') const bindingPackageVersion = require('@gorules/zen-engine-linux-loong64-gnu/package.json').version - if (bindingPackageVersion !== '2.0.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.0.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.0.3-local.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.0.3-local.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -407,8 +407,8 @@ function requireNative() { try { const binding = require('@gorules/zen-engine-linux-riscv64-musl') const bindingPackageVersion = require('@gorules/zen-engine-linux-riscv64-musl/package.json').version - if (bindingPackageVersion !== '2.0.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.0.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.0.3-local.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.0.3-local.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -423,8 +423,8 @@ function requireNative() { try { const binding = require('@gorules/zen-engine-linux-riscv64-gnu') const bindingPackageVersion = require('@gorules/zen-engine-linux-riscv64-gnu/package.json').version - if (bindingPackageVersion !== '2.0.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.0.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.0.3-local.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.0.3-local.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -440,8 +440,8 @@ function requireNative() { try { const binding = require('@gorules/zen-engine-linux-ppc64-gnu') const bindingPackageVersion = require('@gorules/zen-engine-linux-ppc64-gnu/package.json').version - if (bindingPackageVersion !== '2.0.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.0.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.0.3-local.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.0.3-local.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -456,8 +456,8 @@ function requireNative() { try { const binding = require('@gorules/zen-engine-linux-s390x-gnu') const bindingPackageVersion = require('@gorules/zen-engine-linux-s390x-gnu/package.json').version - if (bindingPackageVersion !== '2.0.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.0.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.0.3-local.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.0.3-local.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -476,8 +476,8 @@ function requireNative() { try { const binding = require('@gorules/zen-engine-openharmony-arm64') const bindingPackageVersion = require('@gorules/zen-engine-openharmony-arm64/package.json').version - if (bindingPackageVersion !== '2.0.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.0.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.0.3-local.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.0.3-local.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -492,8 +492,8 @@ function requireNative() { try { const binding = require('@gorules/zen-engine-openharmony-x64') const bindingPackageVersion = require('@gorules/zen-engine-openharmony-x64/package.json').version - if (bindingPackageVersion !== '2.0.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.0.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.0.3-local.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.0.3-local.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { @@ -508,8 +508,8 @@ function requireNative() { try { const binding = require('@gorules/zen-engine-openharmony-arm') const bindingPackageVersion = require('@gorules/zen-engine-openharmony-arm/package.json').version - if (bindingPackageVersion !== '2.0.2' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { - throw new Error(`Native binding package version mismatch, expected 2.0.2 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) + if (bindingPackageVersion !== '2.0.3-local.1' && process.env.NAPI_RS_ENFORCE_VERSION_CHECK && process.env.NAPI_RS_ENFORCE_VERSION_CHECK !== '0') { + throw new Error(`Native binding package version mismatch, expected 2.0.3-local.1 but got ${bindingPackageVersion}. You can reinstall dependencies to fix this issue.`) } return binding } catch (e) { diff --git a/bindings/nodejs/package.json b/bindings/nodejs/package.json index eec287f9..9d8752fa 100644 --- a/bindings/nodejs/package.json +++ b/bindings/nodejs/package.json @@ -1,7 +1,7 @@ { "name": "@gorules/zen-engine", "description": "Open-source Business Rules Engine for Node.js, powered by Rust", - "version": "2.0.2", + "version": "2.0.3-local.1", "main": "index.js", "browser": "browser.js", "types": "./index.d.ts", @@ -101,5 +101,15 @@ }, "resolutions": { "form-data@^4.0.0": "4.0.4" + }, + "optionalDependencies": { + "@gorules/zen-engine-darwin-x64": "2.0.3-local.1", + "@gorules/zen-engine-linux-x64-gnu": "2.0.3-local.1", + "@gorules/zen-engine-linux-x64-musl": "2.0.3-local.1", + "@gorules/zen-engine-win32-x64-msvc": "2.0.3-local.1", + "@gorules/zen-engine-linux-arm64-gnu": "2.0.3-local.1", + "@gorules/zen-engine-linux-arm64-musl": "2.0.3-local.1", + "@gorules/zen-engine-darwin-arm64": "2.0.3-local.1", + "@gorules/zen-engine-wasm32-wasi": "2.0.3-local.1" } -} +} \ No newline at end of file diff --git a/core/engine/src/policy/queries/dependency.rs b/core/engine/src/policy/queries/dependency.rs index dcb1b08a..5898ed40 100644 --- a/core/engine/src/policy/queries/dependency.rs +++ b/core/engine/src/policy/queries/dependency.rs @@ -64,7 +64,7 @@ pub struct EnrichedState { base_fields: HashMap, VariableType>, owned: RefCell>, write_log: Vec<(PropertyPath, VariableType)>, - log_start: HashMap, + own_writes: HashMap>, block_scopes: RefCell>, } @@ -77,8 +77,8 @@ impl Drop for EnrichedState { } impl EnrichedState { - pub(crate) fn scope_before(&self, block: &BlockRef) -> VariableType { - let Some(&end) = self.log_start.get(block) else { + pub(crate) fn scope_excluding(&self, block: &BlockRef) -> VariableType { + let Some(own) = self.own_writes.get(block) else { return self.scope.shallow_clone(); }; if let Some(cached) = self.block_scopes.borrow().get(block) { @@ -87,7 +87,10 @@ impl EnrichedState { let scope = VariableType::Object(Rc::new(RefCell::new(self.base_fields.clone()))).isolated_clone(); self.owned.borrow_mut().push(scope.shallow_clone()); - for (path, resolved_type) in &self.write_log[..end] { + let others = self.write_log[..own.start] + .iter() + .chain(&self.write_log[own.end..]); + for (path, resolved_type) in others { scope.insert_at_path(path, &resolved_type.isolated_clone(), true); } self.block_scopes @@ -441,6 +444,13 @@ impl Snapshot { let entity_form_map = EntityForm::new(entity_sources); let entity_form = |path: &str| -> Option { entity_form_map.rewrite(path) }; + let iterated_entity = |path: &str| { + path.split_once('.').is_some_and(|(root, _)| { + entity_sources + .get(root) + .is_some_and(|src| src.path.as_ref() != root) + }) + }; for (rank, &rule) in per_rule.iter().enumerate() { for read in &rule.reads { @@ -482,7 +492,11 @@ impl Snapshot { block_id: rule.block_id.clone(), }); node.instance_source = write.instance_source.clone(); - node.rank = rank + 1; + node.rank = if iterated_entity(&write.path) { + 0 + } else { + rank + 1 + }; } let path = write.path.as_ref(); @@ -673,7 +687,7 @@ impl Snapshot { }; let mut write_log: Vec<(PropertyPath, VariableType)> = Vec::new(); let mut owned: Vec = Vec::new(); - let mut log_start: HashMap = HashMap::new(); + let mut own_writes: HashMap> = HashMap::new(); let mut per_rule: Vec = Vec::new(); let mut diagnostics: Vec = Vec::new(); @@ -720,7 +734,7 @@ impl Snapshot { continue; }; let policy_path = &key.policy_path; - log_start.insert(key.clone(), write_log.len()); + let start = write_log.len(); let summary = Self::analyze_block( rule, policy_path, @@ -738,6 +752,7 @@ impl Snapshot { write_log.push((tw.path.clone(), frozen)); } } + own_writes.insert(key.clone(), start..write_log.len()); if splice { for tw in &summary.writes { @@ -771,7 +786,7 @@ impl Snapshot { base_fields, owned: RefCell::new(owned), write_log, - log_start, + own_writes, block_scopes: RefCell::new(HashMap::new()), } } diff --git a/core/engine/src/workspace/editor.rs b/core/engine/src/workspace/editor.rs index 1d5ef7c4..76ccb6f5 100644 --- a/core/engine/src/workspace/editor.rs +++ b/core/engine/src/workspace/editor.rs @@ -4,6 +4,7 @@ use ahash::{HashMap, HashMapExt}; use serde_json::Value; use zen_expression::intellisense::completion::Completions; use zen_expression::intellisense::Reference; +use zen_expression::slot::SlotRole; use zen_expression::variable::VariableType; use crate::policy::blocks::{IntelliSenseSource, ROW_ID_KEY}; @@ -215,7 +216,10 @@ impl Db { policy_path: cursor.policy_path.clone(), block_id: cursor.block_id.clone(), }; - let scope = self.cursor_scope(cursor)?; + let mut scope = self.cursor_scope(cursor)?; + if matches!(scope.role, SlotRole::Path) { + scope.scope = self.enriched(&cursor.policy_path).scope.shallow_clone(); + } let policy = self.raw_policy(&cursor.policy_path)?; let block = policy .blocks diff --git a/core/engine/src/workspace/graph/analysis.rs b/core/engine/src/workspace/graph/analysis.rs index e34862f0..cade1cae 100644 --- a/core/engine/src/workspace/graph/analysis.rs +++ b/core/engine/src/workspace/graph/analysis.rs @@ -6,8 +6,9 @@ use ahash::{HashMap, HashMapExt, HashSet}; use zen_expression::variable::VariableType; use zen_types::decision::{ DecisionNode, DecisionNodeContent, DecisionNodeKind, DecisionTableContent, - DecisionTableHitPolicy, DecisionTableOutputField, ExpressionNodeContent, FunctionNodeContent, - SwitchNodeContent, SwitchStatementHitPolicy, TransformAttributes, TransformExecutionMode, + DecisionTableHitPolicy, DecisionTableOutputField, Expression, ExpressionNodeContent, + FunctionNodeContent, SwitchNodeContent, SwitchStatementHitPolicy, TransformAttributes, + TransformExecutionMode, }; use zen_expression::intellisense::ArmTest; @@ -38,7 +39,7 @@ pub struct GraphNodeAnalysis { pub input: VariableType, pub handler_input: VariableType, pub output: VariableType, - pub dollar: Option, + pub row_types: HashMap, VariableType>, pub nodes_scope: VariableType, pub branch_outputs: HashMap, VariableType>, pub opaque: bool, @@ -46,6 +47,18 @@ pub struct GraphNodeAnalysis { pub open: bool, } +impl GraphNodeAnalysis { + pub(crate) fn dollar_before(&self, rows: &[Expression], row_id: &str) -> VariableType { + let dollar = VariableType::empty_object(); + for row in rows.iter().take_while(|row| row.id.as_ref() != row_id) { + if let Some(resolved) = self.row_types.get(&row.id) { + dollar.insert_at_path(&row.key, resolved, true); + } + } + dollar + } +} + #[derive(Debug)] pub struct GraphAnalysis { pub diagnostics: Vec, @@ -449,7 +462,7 @@ impl<'a> GraphAnalyzer<'a> { input: scope_input.shallow_clone(), handler_input: scope_input.shallow_clone(), output: VariableType::Any, - dollar: None, + row_types: HashMap::default(), nodes_scope: self.nodes_scope.shallow_clone(), branch_outputs: HashMap::default(), opaque: false, @@ -523,8 +536,9 @@ impl<'a> GraphAnalyzer<'a> { &content.transform_attributes, &scope_input, |analyzer, scope| { - let (output, dollar) = analyzer.check_expression_rows(node, content, scope); - analysis.dollar = Some(dollar); + let (output, row_types) = + analyzer.check_expression_rows(node, content, scope); + analysis.row_types = row_types; output }, ); @@ -757,9 +771,10 @@ impl<'a> GraphAnalyzer<'a> { node: &DecisionNode, content: &ExpressionNodeContent, scope: &VariableType, - ) -> (VariableType, VariableType) { + ) -> (VariableType, HashMap, VariableType>) { let output = VariableType::empty_object(); let dollar = VariableType::empty_object(); + let mut row_types = HashMap::with_capacity(content.expressions.len()); for row in content.expressions.iter() { if row.key.is_empty() || row.value.is_empty() { continue; @@ -781,8 +796,9 @@ impl<'a> GraphAnalyzer<'a> { ); output.insert_at_path(&row.key, &resolved, true); dollar.insert_at_path(&row.key, &resolved, true); + row_types.insert(row.id.clone(), resolved); } - (output, dollar) + (output, row_types) } fn check_decision_table( diff --git a/core/engine/src/workspace/graph/cursor.rs b/core/engine/src/workspace/graph/cursor.rs index 4c6d0470..ff9e9eba 100644 --- a/core/engine/src/workspace/graph/cursor.rs +++ b/core/engine/src/workspace/graph/cursor.rs @@ -53,14 +53,10 @@ impl Db { return None; }; let row = content.expressions.iter().find(|row| row.id == *id)?; - let dollar = node_analysis - .dollar - .clone() - .unwrap_or_else(VariableType::empty_object); let scope = GraphAnalyzer::scope_with( &node_analysis.handler_input, &[ - ("$", dollar), + ("$", node_analysis.dollar_before(&content.expressions, id)), ("$nodes", node_analysis.nodes_scope.shallow_clone()), ], ); diff --git a/core/engine/src/workspace/slot/scope.rs b/core/engine/src/workspace/slot/scope.rs index 7a4596cf..565c4ecf 100644 --- a/core/engine/src/workspace/slot/scope.rs +++ b/core/engine/src/workspace/slot/scope.rs @@ -29,7 +29,7 @@ impl Db { let block = self.block_ir(&block_ref)?; let unit = self.unit(&cursor.policy_path); let enriched = self.enriched_of_unit(&unit); - let scope = enriched.scope_before(&block_ref); + let scope = enriched.scope_excluding(&block_ref); match (&block.kind, &cursor.target) { (BlockKind::DecisionTable(table), _) => { self.policy_table_scope(&unit, table, cursor, scope, &enriched.scope, cache) @@ -173,14 +173,10 @@ impl Db { DecisionNodeKind::ExpressionNode { content: rows }, CursorTarget::Expression { id }, ) => { - let dollar = node_analysis - .dollar - .clone() - .unwrap_or_else(VariableType::empty_object); let scope = GraphAnalyzer::scope_with( &node_analysis.handler_input, &[ - ("$", dollar), + ("$", node_analysis.dollar_before(&rows.expressions, id)), ("$nodes", node_analysis.nodes_scope.shallow_clone()), ], ); diff --git a/core/engine/tests/data/policy/completions.toml b/core/engine/tests/data/policy/completions.toml index 728b458a..026cc272 100644 --- a/core/engine/tests/data/policy/completions.toml +++ b/core/engine/tests/data/policy/completions.toml @@ -9,7 +9,8 @@ # so top-level cases assert all entities are offered; the meaningful scoping # checks live at the property level (after a dot). -policies = ["analysis.json"] +policies = ["analysis.json", "completion_scope.json"] +graphs = ["completion_scope_graph.json"] [[test]] name = "expression top-level: all entities offered" @@ -59,3 +60,31 @@ expression_id = "o1" row = "r1" pos = 0 includes = ["customer"] + +[[test]] +name = "field written by an independent later block is offered, own write is not" +policy = "completion_scope.json" +block_id = "b1" +expression_id = "b1" +pos = 2 +includes = ["z"] +excludes = ["x"] + +[[test]] +name = "half-typed write target is not offered as its own completion" +policy = "completion_scope.json" +block_id = "b3" +expression_id = "b3" +pos = 4 +key = true +includes = ["part"] +excludes = ["pa"] + +[[test]] +name = "graph expression row sees only earlier rows through $" +policy = "completion_scope_graph.json" +block_id = "calc" +expression_id = "e2" +pos = 2 +includes = ["total"] +excludes = ["vip", "extra"] diff --git a/core/engine/tests/data/policy/diagnostics.toml b/core/engine/tests/data/policy/diagnostics.toml index 146b2590..8256dd4b 100644 --- a/core/engine/tests/data/policy/diagnostics.toml +++ b/core/engine/tests/data/policy/diagnostics.toml @@ -1380,3 +1380,96 @@ content = ''' no_errors = true hint_codes = ["PreferDictionary"] hint_count = 1 + +[[test]] +name = "computed entity field read through a filtered list declared later is not flagged" +content = ''' +{ + "blocks": [ + { + "id": "dmc", + "type": "dataModel", + "props": { + "data": { + "name": "customer", + "properties": [ + { + "id": "p1", + "name": "name", + "type": "string", + "array": false, + "optional": false + }, + { + "id": "p2", + "name": "companies", + "type": "relationship", + "target": "company", + "array": true, + "optional": false + } + ] + } + }, + "children": [] + }, + { + "id": "dmco", + "type": "dataModel", + "props": { + "data": { + "name": "company", + "properties": [ + { + "id": "q1", + "name": "id", + "type": "string", + "array": false, + "optional": false + }, + { + "id": "q2", + "name": "revenue", + "type": "number", + "array": false, + "optional": false + } + ] + } + }, + "children": [] + }, + { + "id": "b1", + "type": "expression", + "props": { + "data": { + "key": "customer.big", + "value": "filter(customer.companies, #.revenue > 0)" + } + } + }, + { + "id": "b2", + "type": "expression", + "props": { + "data": { + "key": "customer.margins", + "value": "map(customer.big as c, c.margin)" + } + } + }, + { + "id": "b3", + "type": "expression", + "props": { + "data": { + "key": "company.margin", + "value": "company.revenue * 0.1" + } + } + } + ] +} +''' +no_errors = true diff --git a/core/engine/tests/data/policy/fixtures/completion_scope.json b/core/engine/tests/data/policy/fixtures/completion_scope.json new file mode 100644 index 00000000..e58b3df5 --- /dev/null +++ b/core/engine/tests/data/policy/fixtures/completion_scope.json @@ -0,0 +1,63 @@ +{ + "blocks": [ + { + "id": "dm", + "type": "dataModel", + "props": { + "data": { + "name": "c", + "properties": [ + { + "id": "p1", + "name": "a", + "type": "number", + "array": false, + "optional": false + } + ] + } + }, + "children": [] + }, + { + "id": "b1", + "type": "expression", + "props": { + "data": { + "key": "r.x", + "value": "r." + } + } + }, + { + "id": "b2", + "type": "expression", + "props": { + "data": { + "key": "r.z", + "value": "c.a * 3" + } + } + }, + { + "id": "b3", + "type": "expression", + "props": { + "data": { + "key": "r.pa", + "value": "1" + } + } + }, + { + "id": "b4", + "type": "expression", + "props": { + "data": { + "key": "r.part", + "value": "2" + } + } + } + ] +} diff --git a/core/engine/tests/data/policy/fixtures/completion_scope_graph.json b/core/engine/tests/data/policy/fixtures/completion_scope_graph.json new file mode 100644 index 00000000..56581367 --- /dev/null +++ b/core/engine/tests/data/policy/fixtures/completion_scope_graph.json @@ -0,0 +1,72 @@ +{ + "nodes": [ + { + "id": "in", + "type": "inputNode", + "name": "in", + "position": { + "x": 0, + "y": 0 + }, + "content": { + "schema": "{\"type\": \"object\", \"properties\": {\"age\": {\"type\": \"number\"}}, \"required\": [\"age\"]}" + } + }, + { + "id": "calc", + "type": "expressionNode", + "name": "calc", + "position": { + "x": 0, + "y": 100 + }, + "content": { + "expressions": [ + { + "id": "e1", + "key": "total", + "value": "age * 2" + }, + { + "id": "e2", + "key": "vip", + "value": "$." + }, + { + "id": "e3", + "key": "extra", + "value": "1" + } + ], + "passThrough": true, + "inputField": null, + "outputPath": null, + "executionMode": "single" + } + }, + { + "id": "out", + "type": "outputNode", + "name": "out", + "position": { + "x": 0, + "y": 200 + }, + "content": {} + } + ], + "edges": [ + { + "id": "x1", + "sourceId": "in", + "targetId": "calc", + "type": "edge" + }, + { + "id": "x2", + "sourceId": "calc", + "targetId": "out", + "type": "edge" + } + ] +} diff --git a/core/engine/tests/policy_toml.rs b/core/engine/tests/policy_toml.rs index 654212c5..45b4248a 100644 --- a/core/engine/tests/policy_toml.rs +++ b/core/engine/tests/policy_toml.rs @@ -22,6 +22,16 @@ fn build_workspace(policies: &[String]) -> PolicyWorkspace { ws } +fn load_graphs(ws: &mut PolicyWorkspace, graphs: &[String]) { + for path in graphs { + let raw = std::fs::read_to_string(format!("{FIXTURES_DIR}{path}")) + .unwrap_or_else(|e| panic!("cannot read fixture {path}: {e}")); + let doc: DecisionContent = serde_json::from_str(&raw) + .unwrap_or_else(|e| panic!("cannot deserialize fixture {path}: {e}")); + ws.set_document(path.as_str(), doc); + } +} + fn toml_to_json(value: &toml::Value) -> serde_json::Value { serde_json::to_value(value).expect("toml converts to json") } @@ -318,6 +328,8 @@ fn run_prepare_rename(file_name: &str, toml_data: &str) { #[derive(Debug, Deserialize)] struct CompletionsFile { policies: Vec, + #[serde(default)] + graphs: Vec, test: Vec, } @@ -330,6 +342,8 @@ struct CompletionsCase { pos: u32, #[serde(default)] head: bool, + #[serde(default)] + key: bool, row: Option, #[serde(default)] includes: Vec, @@ -340,11 +354,14 @@ struct CompletionsCase { fn run_completions(file_name: &str, toml_data: &str) { let file: CompletionsFile = toml::from_str(toml_data).unwrap_or_else(|e| panic!("cannot parse {file_name}: {e}")); - let ws = build_workspace(&file.policies); + let mut ws = build_workspace(&file.policies); + load_graphs(&mut ws, &file.graphs); for test in &file.test { let ctx = format!("[{file_name}:{}]", test.name); - let target = if test.head { + let target = if test.key { + CursorTarget::ExpressionKey + } else if test.head { CursorTarget::DecisionTableHead { col: Arc::from(test.expression_id.as_str()), } @@ -491,13 +508,7 @@ fn run_slots(file_name: &str, toml_data: &str) { let file: SlotsFile = toml::from_str(toml_data).unwrap_or_else(|e| panic!("cannot parse {file_name}: {e}")); let mut ws = build_workspace(&file.policies); - for path in &file.graphs { - let raw = std::fs::read_to_string(format!("{FIXTURES_DIR}{path}")) - .unwrap_or_else(|e| panic!("cannot read fixture {path}: {e}")); - let doc: DecisionContent = serde_json::from_str(&raw) - .unwrap_or_else(|e| panic!("cannot deserialize fixture {path}: {e}")); - ws.set_document(path.as_str(), doc); - } + load_graphs(&mut ws, &file.graphs); for test in &file.test { test.check(&ws); } diff --git a/core/expression/src/intellisense/completion.rs b/core/expression/src/intellisense/completion.rs index 4c796b89..76b3aaed 100644 --- a/core/expression/src/intellisense/completion.rs +++ b/core/expression/src/intellisense/completion.rs @@ -64,7 +64,9 @@ impl Completions { Self::build_property(slot.operand.as_ref().unwrap_or(&VariableType::Any)) } SlotState::Path if slot.operand.is_some() => { - Self::build_property(slot.operand.as_ref().unwrap()) + let mut items = Self::build_property(slot.operand.as_ref().unwrap()); + items.retain(|item| item.kind != CompletionKind::Method); + items } _ => Self::build_scope(data, &locals), }; diff --git a/core/expression/src/parser/ast.rs b/core/expression/src/parser/ast.rs index 51edb404..92688aed 100644 --- a/core/expression/src/parser/ast.rs +++ b/core/expression/src/parser/ast.rs @@ -261,7 +261,7 @@ pub enum AstNodeError<'a> { #[error("Expected a literal at ({}, {})", span.0, span.1)] ExpectedLiteral { span: (u32, u32) }, - #[error("Unexpected end of unary expression at ({}, {})", span.0, span.1)] + #[error("Unexpected end of expression at ({}, {})", span.0, span.1)] UnexpectedEnd { span: (u32, u32) }, #[error("{message} at ({}, {})", span.0, span.1)] diff --git a/core/expression/src/slot/classify.rs b/core/expression/src/slot/classify.rs index f8ab405b..20368b5f 100644 --- a/core/expression/src/slot/classify.rs +++ b/core/expression/src/slot/classify.rs @@ -418,6 +418,11 @@ impl<'p, 'a> Classifier<'p, 'a> { if self.role == SlotRole::Path { return self.path_slot(pos); } + if self.in_decimal(pos) { + let mut slot = Slot::new(SlotState::Value, (pos, pos)); + slot.suppress_completions = true; + return slot; + } let limit = self.operand_limit(pos); if let Some(frame) = self.frames(limit).last() { @@ -524,6 +529,18 @@ impl<'p, 'a> Classifier<'p, 'a> { } } + fn in_decimal(&self, pos: u32) -> bool { + let before = self.text((0, pos)); + let fraction = before.trim_end_matches(|c: char| c.is_ascii_digit()); + let Some(integer) = fraction.strip_suffix('.') else { + return false; + }; + let head = integer.trim_end_matches(|c: char| c.is_ascii_digit() || c == '_'); + head.len() < integer.len() + && !head.ends_with(|c: char| c.is_alphanumeric() || matches!(c, '_' | '$' | '#' | '.')) + && self.string_at(pos).is_none() + } + fn text(&self, span: Span) -> &str { self.source .get(span.0 as usize..span.1 as usize) @@ -1191,7 +1208,7 @@ impl<'p, 'a> Classifier<'p, 'a> { let start = self.run_start(end, false); slot.operand = self.type_of_run(start, end, true); let head = self.at(self.items[start].span.0, start); - if head.wanted_scalar().is_some() && head.state != SlotState::Member { + if head.wanted_scalar().is_some() { slot.expected = head.expected; } } diff --git a/core/expression/src/slot/mod.rs b/core/expression/src/slot/mod.rs index c9e54cb5..6fffc38f 100644 --- a/core/expression/src/slot/mod.rs +++ b/core/expression/src/slot/mod.rs @@ -92,12 +92,13 @@ impl Slot { | SlotState::ListElement | SlotState::Range | SlotState::Argument - | SlotState::Member ) { return None; } let (t, _) = self.expected.as_ref()?.unwrap_nullable(); - ScalarClass::of(t).map(|_| t) + ScalarClass::of(t) + .filter(|class| *class != ScalarClass::Bool) + .map(|_| t) } pub(crate) fn new(state: SlotState, replace_span: Span) -> Self { diff --git a/core/expression/tests/analysis.rs b/core/expression/tests/analysis.rs index e54613e0..57a2c6f9 100644 --- a/core/expression/tests/analysis.rs +++ b/core/expression/tests/analysis.rs @@ -61,6 +61,7 @@ struct ExpectedDiagnostic { source: String, severity: String, code: Option, + message: Option, #[serde(default)] args: std::collections::BTreeMap, } @@ -320,6 +321,10 @@ fn check_diagnostics( .code .as_deref() .is_none_or(|code| d.code == Some(code)) + && expected + .message + .as_deref() + .is_none_or(|message| d.message.starts_with(message)) && expected .args .iter() @@ -328,8 +333,8 @@ fn check_diagnostics( assert!( matching, - "[{file_name}:{}:{mode}] Diagnostic #{i} not found: expected source={}, severity={}, code={:?}, args={:?}.\n Expression: {}\n Got diagnostics: {:?}", - test.name, expected.source, expected.severity, expected.code, expected.args, test.expression, result.diagnostics + "[{file_name}:{}:{mode}] Diagnostic #{i} not found: expected source={}, severity={}, code={:?}, message={:?}, args={:?}.\n Expression: {}\n Got diagnostics: {:?}", + test.name, expected.source, expected.severity, expected.code, expected.message, expected.args, test.expression, result.diagnostics ); } } diff --git a/core/expression/tests/data/analysis/diagnostics.toml b/core/expression/tests/data/analysis/diagnostics.toml index 7be267ea..bef55030 100644 --- a/core/expression/tests/data/analysis/diagnostics.toml +++ b/core/expression/tests/data/analysis/diagnostics.toml @@ -355,3 +355,13 @@ diagnostics = [ { source = "parser", severity = "error", code = "expr.expected-property" }, ] + +[[test]] +name = "code: missing value after operator" +expression = "a *" +input = '{"a": 1}' + +[test.loose] +diagnostics = [ + { source = "parser", severity = "error", code = "expr.missing-value", message = "Unexpected end of expression", args = { operator = "*" } }, +] diff --git a/core/expression/tests/data/slots.toml b/core/expression/tests/data/slots.toml index ce24b85b..fb78f954 100644 --- a/core/expression/tests/data/slots.toml +++ b/core/expression/tests/data/slots.toml @@ -507,3 +507,53 @@ literals = ["d(\"2024-01-01\", \"Europe/Berlin\") date 2024-01-01 tz=Europe/Berl [[test]] expression = "since > d(customer.since) and" literals = ["d(customer.since) date field customer.since"] + +# Completions filtered by expected type + +[[test]] +expression = "s|" +role = "value" +expected = "bool" +includes = ["status", "since"] + +[[test]] +expression = "customer.|" +role = "value" +expected = "bool" +includes = ["name", "age", "address"] + +[[test]] +expression = "customer.|" +role = "value" +expected = "number" +includes = ["name", "age", "address"] + +[[test]] +expression = "|" +role = "value" +expected = "number" +includes = ["age", "customer"] +excludes = ["name", "since"] + +# Decimal literals and write paths + +[[test]] +expression = "0.|" +role = "value" +expected = "number" +slot = { state = "value" } +excludes = ["age", "customer", "len"] + +[[test]] +expression = "age > 12.|5" +excludes = ["age", "customer"] + +[[test]] +expression = "customer.|" +includes = ["age", "name"] + +[[test]] +expression = "data.|" +role = "path" +scope = '{"Object":{"data":"Any"}}' +excludes = ["add", "format", "year"] diff --git a/core/expression/tests/slots.rs b/core/expression/tests/slots.rs index c1309b39..7388e2ff 100644 --- a/core/expression/tests/slots.rs +++ b/core/expression/tests/slots.rs @@ -1,6 +1,7 @@ use std::rc::Rc; use serde::Deserialize; +use zen_expression::intellisense::completion::Completions; use zen_expression::intellisense::IntelliSense; use zen_expression::slot::{DateArg, LabelResolver, LiteralFact, Literals, Slot, SlotRole}; use zen_expression::variable::VariableType; @@ -35,6 +36,10 @@ struct TestCase { expected: Option, slot: Option, literals: Option>, + #[serde(default)] + includes: Vec, + #[serde(default)] + excludes: Vec, } #[derive(Deserialize)] @@ -80,12 +85,13 @@ impl TestCase { Some(caret) => (self.expression.replacen('|', "", 1), caret), None => (self.expression.clone(), self.expression.len()), }; + let scope = self.scope(); let result = is.slot( &source, pos as u32, self.subject.is_some(), self.role(), - &self.scope(), + &scope, self.expected().as_ref(), ); let mut failures = Vec::new(); @@ -98,6 +104,19 @@ impl TestCase { failures.push(format!("literals: wanted {expected:?}, got {actual:?}")); } } + if !self.includes.is_empty() || !self.excludes.is_empty() { + let labels: Vec = + Completions::from_slot(&source, pos as u32, &scope, &result.slot) + .into_iter() + .map(|c| c.label) + .collect(); + for label in self.includes.iter().filter(|l| !labels.contains(l)) { + failures.push(format!("completion {label} missing from {labels:?}")); + } + for label in self.excludes.iter().filter(|l| labels.contains(l)) { + failures.push(format!("completion {label} unexpected in {labels:?}")); + } + } failures } }