mirror of
https://github.com/gorules/zen.git
synced 2026-10-04 00:02:18 +00:00
* feat: implement slots instead of natural language Replace the natural-language projection (`nl`) with engine-owned slot completion: for a cursor position the engine classifies the slot being edited (operand, operator, literal, member access, ...) and returns typed options, literal facts and the expected type, for policies and graphs. - zen-expression: new `slot` module (classification, literals, operators) and a lenient lexer mode for incomplete input; `nl` removed - Intellisense: typed diagnostic codes with arguments, richer inspect for hover, dedicated parser error variants (messages unchanged) - Engine: `Workspace::cursor_scope`, `slot`, `facts`; `rename_from` and `references_from`; editor spans in UTF-16 units; renames reach imported policies, `$nodes` reads, child graphs and output column heads; no `$root` completion in policies - Node bindings: `slot`, `facts`, `cursorScope`, `slotBatch` replace `nl`, `nlTokenize`, `nlEncodeString`, `nlTokenizeBatch` No change to expression evaluation: the VM, compiler, functions and the default lexer path are untouched, and parser error messages keep their text. * fix: harden slot classification, scopes and graph expectations - restore master execution order; slot scopes hide writes of the edited block and its dependents instead of relying on block ranking - token-start editing gated by slot state; operator, list and operand spans no longer splice into neighbouring tokens - interval-aware bracket pairing for half-open and reversed intervals - lenient lexer recovers from unknown characters (slots only) - graph expected type follows switch and pass-through nodes and merges compatible output schemas - sibling inference caps distinct values and analyses lazily - deterministic overload return type display * fix: order policy blocks by entity reads through closures and relationships - record reads for `#.field` in closures so entity fields read through lists become dependencies - link reads through derived lists (filter/map results) to the entity fields they carry and order the list after those fields - evaluate entity blocks on single relationships, and demand entity paths for plain reads through relationships - slot scopes keep the top-level variable when hiding written fields
685 lines
22 KiB
Rust
685 lines
22 KiB
Rust
use serde::Deserialize;
|
|
use serde_json::Value;
|
|
use zen_expression::intellisense::dependency::{ReadDependency, Reference};
|
|
|
|
use zen_expression::intellisense::diagnostic::{DiagnosticSource, Severity};
|
|
use zen_expression::intellisense::{ExpressionAnalysis, IntelliSense};
|
|
use zen_expression::slot::SlotRole;
|
|
use zen_expression::variable::VariableType;
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct TestFile {
|
|
test: Vec<TestCase>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct TestCase {
|
|
name: String,
|
|
expression: String,
|
|
input: Option<String>,
|
|
return_type: Option<String>,
|
|
reads: Option<Vec<ReadDependency>>,
|
|
reads_with_spans: Option<Vec<ReadDependency>>,
|
|
references: Option<Vec<Reference>>,
|
|
diagnostics: Option<Vec<ExpectedDiagnostic>>,
|
|
#[serde(default)]
|
|
unary: bool,
|
|
#[serde(default)]
|
|
strict: StrictField,
|
|
loose: Option<ModeExpectations>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Default)]
|
|
#[serde(untagged)]
|
|
enum StrictField {
|
|
#[default]
|
|
Absent,
|
|
Bool(bool),
|
|
Expectations(ModeExpectations),
|
|
}
|
|
|
|
impl StrictField {
|
|
fn as_bool(&self) -> bool {
|
|
matches!(self, StrictField::Bool(true))
|
|
}
|
|
|
|
fn as_expectations(&self) -> Option<&ModeExpectations> {
|
|
match self {
|
|
StrictField::Expectations(e) => Some(e),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize, Default)]
|
|
struct ModeExpectations {
|
|
return_type: Option<String>,
|
|
diagnostics: Option<Vec<ExpectedDiagnostic>>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct ExpectedDiagnostic {
|
|
source: String,
|
|
severity: String,
|
|
code: Option<String>,
|
|
#[serde(default)]
|
|
args: std::collections::BTreeMap<String, String>,
|
|
}
|
|
|
|
fn parse_data_type(test: &TestCase) -> VariableType {
|
|
match &test.input {
|
|
Some(input_str) => {
|
|
// Try parsing as VariableType directly (for enum types etc.),
|
|
// fall back to JSON Value conversion
|
|
serde_json::from_str::<VariableType>(input_str).unwrap_or_else(|_| {
|
|
let input: Value = serde_json5::from_str(input_str)
|
|
.unwrap_or_else(|e| panic!("[{}] Failed to parse input: {e}", test.name));
|
|
VariableType::from(input)
|
|
})
|
|
}
|
|
None => VariableType::Any,
|
|
}
|
|
}
|
|
|
|
fn run_analysis_inner(test: &TestCase, strict: bool, unary: bool) -> ExpressionAnalysis {
|
|
let mut is = IntelliSense::new().with_strict(strict);
|
|
let data_type = parse_data_type(test);
|
|
let rc = if unary {
|
|
is.analyze_unary(&test.expression, &data_type)
|
|
} else {
|
|
is.analyze(&test.expression, &data_type)
|
|
};
|
|
std::rc::Rc::unwrap_or_clone(rc)
|
|
}
|
|
|
|
fn parse_diagnostic_source(s: &str) -> DiagnosticSource {
|
|
match s {
|
|
"lexer" => DiagnosticSource::Lexer,
|
|
"parser" => DiagnosticSource::Parser,
|
|
"type_check" => DiagnosticSource::TypeCheck,
|
|
"compiler" => DiagnosticSource::Compiler,
|
|
other => panic!("Unknown diagnostic source: {other}"),
|
|
}
|
|
}
|
|
|
|
fn parse_severity(s: &str) -> Severity {
|
|
match s {
|
|
"error" => Severity::Error,
|
|
"warning" => Severity::Warning,
|
|
other => panic!("Unknown severity: {other}"),
|
|
}
|
|
}
|
|
|
|
fn run_test_file(file_name: &str, toml_data: &str) {
|
|
run_test_file_inner(file_name, toml_data, false);
|
|
}
|
|
|
|
fn run_test_file_unary(file_name: &str, toml_data: &str) {
|
|
run_test_file_inner(file_name, toml_data, true);
|
|
}
|
|
|
|
fn run_test_file_inner(file_name: &str, toml_data: &str, force_unary: bool) {
|
|
let test_file: TestFile =
|
|
toml::from_str(toml_data).unwrap_or_else(|e| panic!("Failed to parse {file_name}: {e}"));
|
|
|
|
for test in &test_file.test {
|
|
let unary = test.unary || force_unary;
|
|
let has_mode_sections = test.loose.is_some() || test.strict.as_expectations().is_some();
|
|
|
|
if has_mode_sections {
|
|
if let Some(loose) = &test.loose {
|
|
let result = run_analysis_inner(test, false, unary);
|
|
check_mode(file_name, test, &result, loose, "loose");
|
|
}
|
|
if let Some(strict) = test.strict.as_expectations() {
|
|
let result = run_analysis_inner(test, true, unary);
|
|
check_mode(file_name, test, &result, strict, "strict");
|
|
}
|
|
} else {
|
|
let result = run_analysis_inner(test, test.strict.as_bool(), unary);
|
|
check_result(file_name, test, &result);
|
|
}
|
|
}
|
|
}
|
|
|
|
fn check_mode(
|
|
file_name: &str,
|
|
test: &TestCase,
|
|
result: &ExpressionAnalysis,
|
|
expected: &ModeExpectations,
|
|
mode: &str,
|
|
) {
|
|
if let Some(expected_rt) = &expected.return_type {
|
|
let expected_type: VariableType = serde_json::from_str(expected_rt).unwrap_or_else(|e| {
|
|
panic!(
|
|
"[{file_name}:{}:{mode}] Failed to parse return_type '{}': {e}",
|
|
test.name, expected_rt
|
|
)
|
|
});
|
|
assert_eq!(
|
|
result.return_type, expected_type,
|
|
"[{file_name}:{}:{mode}] return_type mismatch.\n Expression: {}\n Expected: {:?}\n Got: {:?}",
|
|
test.name, test.expression, expected_type, result.return_type
|
|
);
|
|
}
|
|
|
|
if let Some(expected_reads) = &test.reads {
|
|
let actual_stripped: Vec<_> = result.reads.iter().map(|r| r.without_spans()).collect();
|
|
let expected_stripped: Vec<_> = expected_reads.iter().map(|r| r.without_spans()).collect();
|
|
assert_eq!(
|
|
actual_stripped,
|
|
expected_stripped,
|
|
"[{file_name}:{}:{mode}] reads mismatch.\n Expression: {}\n Expected: {}\n Got: {}",
|
|
test.name,
|
|
test.expression,
|
|
serde_json::to_string_pretty(expected_reads).unwrap(),
|
|
serde_json::to_string_pretty(&result.reads).unwrap()
|
|
);
|
|
}
|
|
|
|
if let Some(expected_reads) = &test.reads_with_spans {
|
|
assert_eq!(
|
|
&result.reads, expected_reads,
|
|
"[{file_name}:{}:{mode}] reads_with_spans mismatch.\n Expression: {}\n Expected: {}\n Got: {}",
|
|
test.name, test.expression,
|
|
serde_json::to_string_pretty(expected_reads).unwrap(),
|
|
serde_json::to_string_pretty(&result.reads).unwrap()
|
|
);
|
|
}
|
|
|
|
if let Some(expected_refs) = &test.references {
|
|
let actual: Vec<_> = result
|
|
.references
|
|
.iter()
|
|
.map(|r| r.without_via_alias())
|
|
.collect();
|
|
let expected: Vec<_> = expected_refs
|
|
.iter()
|
|
.map(|r| r.without_via_alias())
|
|
.collect();
|
|
assert_eq!(
|
|
actual, expected,
|
|
"[{file_name}:{}:{mode}] references mismatch.\n Expression: {}\n Expected: {}\n Got: {}",
|
|
test.name, test.expression,
|
|
serde_json::to_string_pretty(expected_refs).unwrap(),
|
|
serde_json::to_string_pretty(&result.references).unwrap()
|
|
);
|
|
}
|
|
|
|
if let Some(expected_diags) = &expected.diagnostics {
|
|
check_diagnostics(file_name, test, result, expected_diags, mode);
|
|
} else if expected.return_type.is_some() || test.reads.is_some() {
|
|
let errors: Vec<_> = result
|
|
.diagnostics
|
|
.iter()
|
|
.filter(|d| d.severity == Severity::Error)
|
|
.collect();
|
|
assert!(
|
|
errors.is_empty(),
|
|
"[{file_name}:{}:{mode}] Expected no error diagnostics, but got: {:?}\n Expression: {}",
|
|
test.name, errors, test.expression
|
|
);
|
|
}
|
|
}
|
|
|
|
fn check_result(file_name: &str, test: &TestCase, result: &ExpressionAnalysis) {
|
|
let mode = if test.strict.as_bool() {
|
|
"strict"
|
|
} else {
|
|
"loose"
|
|
};
|
|
|
|
if let Some(expected_rt) = &test.return_type {
|
|
let expected: VariableType = serde_json::from_str(expected_rt).unwrap_or_else(|e| {
|
|
panic!(
|
|
"[{file_name}:{}] Failed to parse return_type '{}': {e}",
|
|
test.name, expected_rt
|
|
)
|
|
});
|
|
assert_eq!(
|
|
result.return_type, expected,
|
|
"[{file_name}:{}:{mode}] return_type mismatch.\n Expression: {}\n Expected: {:?}\n Got: {:?}",
|
|
test.name, test.expression, expected, result.return_type
|
|
);
|
|
}
|
|
|
|
if let Some(expected_reads) = &test.reads {
|
|
let actual_stripped: Vec<_> = result.reads.iter().map(|r| r.without_spans()).collect();
|
|
let expected_stripped: Vec<_> = expected_reads.iter().map(|r| r.without_spans()).collect();
|
|
assert_eq!(
|
|
actual_stripped,
|
|
expected_stripped,
|
|
"[{file_name}:{}] reads mismatch.\n Expression: {}\n Expected: {}\n Got: {}",
|
|
test.name,
|
|
test.expression,
|
|
serde_json::to_string_pretty(expected_reads).unwrap(),
|
|
serde_json::to_string_pretty(&result.reads).unwrap()
|
|
);
|
|
}
|
|
|
|
if let Some(expected_reads) = &test.reads_with_spans {
|
|
assert_eq!(
|
|
&result.reads,
|
|
expected_reads,
|
|
"[{file_name}:{}] reads_with_spans mismatch.\n Expression: {}\n Expected: {}\n Got: {}",
|
|
test.name,
|
|
test.expression,
|
|
serde_json::to_string_pretty(expected_reads).unwrap(),
|
|
serde_json::to_string_pretty(&result.reads).unwrap()
|
|
);
|
|
}
|
|
|
|
if let Some(expected_refs) = &test.references {
|
|
assert_eq!(
|
|
&result.references,
|
|
expected_refs,
|
|
"[{file_name}:{}] references mismatch.\n Expression: {}\n Expected: {}\n Got: {}",
|
|
test.name,
|
|
test.expression,
|
|
serde_json::to_string_pretty(expected_refs).unwrap(),
|
|
serde_json::to_string_pretty(&result.references).unwrap()
|
|
);
|
|
}
|
|
|
|
if let Some(expected_diags) = &test.diagnostics {
|
|
check_diagnostics(file_name, test, result, expected_diags, mode);
|
|
} else if test.reads.is_some() || test.return_type.is_some() {
|
|
let errors: Vec<_> = result
|
|
.diagnostics
|
|
.iter()
|
|
.filter(|d| d.severity == Severity::Error)
|
|
.collect();
|
|
assert!(
|
|
errors.is_empty(),
|
|
"[{file_name}:{}:{mode}] Expected no error diagnostics, but got: {:?}\n Expression: {}",
|
|
test.name, errors, test.expression
|
|
);
|
|
}
|
|
}
|
|
|
|
fn check_diagnostics(
|
|
file_name: &str,
|
|
test: &TestCase,
|
|
result: &ExpressionAnalysis,
|
|
expected_diags: &[ExpectedDiagnostic],
|
|
mode: &str,
|
|
) {
|
|
assert!(
|
|
result.diagnostics.len() >= expected_diags.len(),
|
|
"[{file_name}:{}:{mode}] Expected at least {} diagnostics, got {}.\n Expression: {}\n Diagnostics: {:?}",
|
|
test.name, expected_diags.len(), result.diagnostics.len(), test.expression, result.diagnostics
|
|
);
|
|
|
|
for (i, expected) in expected_diags.iter().enumerate() {
|
|
let expected_source = parse_diagnostic_source(&expected.source);
|
|
let expected_severity = parse_severity(&expected.severity);
|
|
|
|
let matching = result.diagnostics.iter().any(|d| {
|
|
d.source == expected_source
|
|
&& d.severity == expected_severity
|
|
&& expected
|
|
.code
|
|
.as_deref()
|
|
.is_none_or(|code| d.code == Some(code))
|
|
&& expected
|
|
.args
|
|
.iter()
|
|
.all(|(k, v)| d.args.get(k.as_str()) == Some(v))
|
|
});
|
|
|
|
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
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn analysis_types() {
|
|
run_test_file("types", include_str!("data/analysis/types.toml"));
|
|
}
|
|
|
|
#[test]
|
|
fn analysis_dependencies() {
|
|
run_test_file(
|
|
"dependencies",
|
|
include_str!("data/analysis/dependencies.toml"),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn analysis_closures() {
|
|
run_test_file("closures", include_str!("data/analysis/closures.toml"));
|
|
}
|
|
|
|
#[test]
|
|
fn analysis_functions() {
|
|
run_test_file("functions", include_str!("data/analysis/functions.toml"));
|
|
}
|
|
|
|
#[test]
|
|
fn analysis_diagnostics() {
|
|
run_test_file(
|
|
"diagnostics",
|
|
include_str!("data/analysis/diagnostics.toml"),
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn analysis_references() {
|
|
run_test_file("references", include_str!("data/analysis/references.toml"));
|
|
}
|
|
|
|
#[test]
|
|
fn analysis_edge_cases() {
|
|
run_test_file("edge_cases", include_str!("data/analysis/edge_cases.toml"));
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct CompletionTestFile {
|
|
test: Vec<CompletionTestCase>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct CompletionTestCase {
|
|
name: String,
|
|
expression: String,
|
|
pos: u32,
|
|
input: Option<String>,
|
|
#[serde(default)]
|
|
includes: Vec<String>,
|
|
#[serde(default)]
|
|
excludes: Vec<String>,
|
|
}
|
|
|
|
fn run_completion_test_file(file_name: &str, toml_data: &str) {
|
|
let test_file: CompletionTestFile =
|
|
toml::from_str(toml_data).unwrap_or_else(|e| panic!("Failed to parse {file_name}: {e}"));
|
|
|
|
for test in &test_file.test {
|
|
let mut is = IntelliSense::new();
|
|
|
|
let data_type = match &test.input {
|
|
Some(input_str) => {
|
|
let input: Value = serde_json5::from_str(input_str).unwrap_or_else(|e| {
|
|
panic!("[{file_name}:{}] Failed to parse input: {e}", test.name)
|
|
});
|
|
VariableType::from(input)
|
|
}
|
|
None => VariableType::Any,
|
|
};
|
|
|
|
let completions = is.completions(&test.expression, test.pos, &data_type);
|
|
let labels: Vec<&str> = completions.iter().map(|c| c.label.as_str()).collect();
|
|
|
|
for inc in &test.includes {
|
|
assert!(
|
|
labels.contains(&inc.as_str()),
|
|
"[{file_name}:{}] Expected completion '{}' not found.\n Expression: {:?} @ pos {}\n Got: {:?}",
|
|
test.name, inc, test.expression, test.pos, labels
|
|
);
|
|
}
|
|
|
|
for exc in &test.excludes {
|
|
assert!(
|
|
!labels.contains(&exc.as_str()),
|
|
"[{file_name}:{}] Unexpected completion '{}' found.\n Expression: {:?} @ pos {}\n Got: {:?}",
|
|
test.name, exc, test.expression, test.pos, labels
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn analysis_completions() {
|
|
run_completion_test_file(
|
|
"completions",
|
|
include_str!("data/analysis/completions.toml"),
|
|
);
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct InspectTestFile {
|
|
test: Vec<InspectTestCase>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
struct InspectTestCase {
|
|
name: String,
|
|
expression: String,
|
|
pos: u32,
|
|
input: Option<String>,
|
|
label: Option<String>,
|
|
kind: Option<String>,
|
|
detail: Option<String>,
|
|
#[serde(default)]
|
|
unary: bool,
|
|
}
|
|
|
|
fn run_inspect_test_file(file_name: &str, toml_data: &str) {
|
|
let test_file: InspectTestFile =
|
|
toml::from_str(toml_data).unwrap_or_else(|e| panic!("Failed to parse {file_name}: {e}"));
|
|
|
|
for test in &test_file.test {
|
|
let mut is = IntelliSense::new();
|
|
|
|
let data_type = match &test.input {
|
|
Some(input_str) => {
|
|
let input: Value = serde_json5::from_str(input_str).unwrap_or_else(|e| {
|
|
panic!("[{file_name}:{}] Failed to parse input: {e}", test.name)
|
|
});
|
|
VariableType::from(input)
|
|
}
|
|
None => VariableType::Any,
|
|
};
|
|
|
|
let role = if test.unary {
|
|
SlotRole::Unary
|
|
} else {
|
|
SlotRole::Value
|
|
};
|
|
let result = is.inspect(&test.expression, test.pos, test.unary, role, &data_type);
|
|
|
|
if test.label.is_some() || test.kind.is_some() || test.detail.is_some() {
|
|
let result = result.unwrap_or_else(|| {
|
|
panic!(
|
|
"[{file_name}:{}] Expected inspect result, got None.\n Expression: {:?} @ pos {}",
|
|
test.name, test.expression, test.pos
|
|
)
|
|
});
|
|
|
|
if let Some(expected_label) = &test.label {
|
|
assert_eq!(
|
|
&result.label, expected_label,
|
|
"[{file_name}:{}] label mismatch.\n Expression: {:?} @ pos {}",
|
|
test.name, test.expression, test.pos
|
|
);
|
|
}
|
|
|
|
if let Some(expected_kind) = &test.kind {
|
|
let expected: VariableType =
|
|
serde_json::from_str(expected_kind).unwrap_or_else(|e| {
|
|
panic!(
|
|
"[{file_name}:{}] Failed to parse kind '{}': {e}",
|
|
test.name, expected_kind
|
|
)
|
|
});
|
|
assert_eq!(
|
|
result.kind, expected,
|
|
"[{file_name}:{}] kind mismatch.\n Expression: {:?} @ pos {}",
|
|
test.name, test.expression, test.pos
|
|
);
|
|
}
|
|
|
|
if let Some(expected_detail) = &test.detail {
|
|
assert!(
|
|
result
|
|
.detail
|
|
.as_deref()
|
|
.is_some_and(|detail| detail.starts_with(expected_detail.as_str())),
|
|
"[{file_name}:{}] detail mismatch: {:?}.\n Expression: {:?} @ pos {}",
|
|
test.name,
|
|
result.detail,
|
|
test.expression,
|
|
test.pos
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn analysis_inspect() {
|
|
run_inspect_test_file("inspect", include_str!("data/analysis/inspect.toml"));
|
|
}
|
|
|
|
#[test]
|
|
fn analysis_unary() {
|
|
run_test_file_unary("unary", include_str!("data/analysis/unary.toml"));
|
|
}
|
|
|
|
#[test]
|
|
fn analysis_enums() {
|
|
run_test_file("enums", include_str!("data/analysis/enums.toml"));
|
|
}
|
|
|
|
#[test]
|
|
fn analysis_read_spans() {
|
|
run_test_file("read_spans", include_str!("data/analysis/read_spans.toml"));
|
|
}
|
|
|
|
#[test]
|
|
fn analysis_nullable() {
|
|
run_test_file("nullable", include_str!("data/analysis/nullable.toml"));
|
|
}
|
|
|
|
#[test]
|
|
#[cfg_attr(miri, ignore)]
|
|
fn analysis_nested_closures_complete_quickly() {
|
|
let depth = 25;
|
|
let mut source = String::new();
|
|
(0..depth).for_each(|_| source.push_str("map([0], "));
|
|
source.push('1');
|
|
(0..depth).for_each(|_| source.push(')'));
|
|
|
|
let start = std::time::Instant::now();
|
|
let mut is = IntelliSense::new().with_strict(true);
|
|
let analysis = is.analyze(&source, &VariableType::Any);
|
|
assert!(
|
|
start.elapsed() < std::time::Duration::from_secs(5),
|
|
"nested closure type-check took {:?}",
|
|
start.elapsed()
|
|
);
|
|
|
|
let errors: Vec<_> = analysis
|
|
.diagnostics
|
|
.iter()
|
|
.filter(|d| d.severity == Severity::Error)
|
|
.collect();
|
|
assert!(errors.is_empty(), "unexpected diagnostics: {errors:?}");
|
|
}
|
|
|
|
#[test]
|
|
fn analysis_nested_closure_shallow_diagnostics_unchanged() {
|
|
let input = VariableType::from(serde_json::json!({"items": [1, 2]}));
|
|
let mut is = IntelliSense::new().with_strict(true);
|
|
let analysis = is.analyze("filter(items, # + 1)", &input);
|
|
assert!(
|
|
analysis
|
|
.diagnostics
|
|
.iter()
|
|
.any(|d| d.severity == Severity::Error && d.source == DiagnosticSource::TypeCheck),
|
|
"expected callback return-type error, got: {:?}",
|
|
analysis.diagnostics
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn analysis_overlapping_enum_equality_has_no_hint() {
|
|
let input: VariableType = serde_json::from_str(
|
|
r#"{"Object":{"a":{"Enum":[null,["a","b"]]},"b":{"Enum":[null,["b","c"]]}}}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
for strict in [false, true] {
|
|
for expression in ["a == b", "a != b"] {
|
|
let mut is = IntelliSense::new().with_strict(strict);
|
|
let analysis = is.analyze(expression, &input);
|
|
assert!(
|
|
analysis.diagnostics.is_empty(),
|
|
"[{expression}:strict={strict}] expected no diagnostics, got: {:?}",
|
|
analysis.diagnostics
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn analysis_structured_equality_hint_fires() {
|
|
let input: VariableType = serde_json::from_str(
|
|
r#"{"Object":{"a":{"Object":{"x":"Number"}},"b":{"Object":{"x":"Number"}},"c":{"Array":"Number"},"d":{"Array":"Number"}}}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
for strict in [false, true] {
|
|
for expression in ["a == b", "a != b", "c == d", "c != d"] {
|
|
let mut is = IntelliSense::new().with_strict(strict);
|
|
let analysis = is.analyze(expression, &input);
|
|
assert!(
|
|
analysis
|
|
.diagnostics
|
|
.iter()
|
|
.any(|d| d.severity == Severity::Warning
|
|
&& d.source == DiagnosticSource::TypeCheck),
|
|
"[{expression}:strict={strict}] expected always-constant hint, got: {:?}",
|
|
analysis.diagnostics
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn analysis_nullable_structured_equality_no_hint() {
|
|
let input: VariableType = serde_json::from_str(
|
|
r#"{"Object":{"a":{"Nullable":{"Object":{"x":"Number"}}},"b":{"Nullable":{"Object":{"x":"Number"}}},"c":{"Object":{"x":"Number"}}}}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
for strict in [false, true] {
|
|
for expression in ["a == b", "a != b", "c == null", "c != null", "a == c"] {
|
|
let mut is = IntelliSense::new().with_strict(strict);
|
|
let analysis = is.analyze(expression, &input);
|
|
assert!(
|
|
analysis.diagnostics.is_empty(),
|
|
"[{expression}:strict={strict}] expected no diagnostics, got: {:?}",
|
|
analysis.diagnostics
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn analysis_disjoint_enum_equality_hint_fires() {
|
|
let input: VariableType = serde_json::from_str(
|
|
r#"{"Object":{"a":{"Enum":[null,["a","b"]]},"b":{"Enum":[null,["c","d"]]}}}"#,
|
|
)
|
|
.unwrap();
|
|
|
|
for strict in [false, true] {
|
|
for expression in ["a == b", "a != b"] {
|
|
let mut is = IntelliSense::new().with_strict(strict);
|
|
let analysis = is.analyze(expression, &input);
|
|
assert!(
|
|
analysis
|
|
.diagnostics
|
|
.iter()
|
|
.any(|d| d.severity == Severity::Warning
|
|
&& d.source == DiagnosticSource::TypeCheck),
|
|
"[{expression}:strict={strict}] expected always-constant hint, got: {:?}",
|
|
analysis.diagnostics
|
|
);
|
|
}
|
|
}
|
|
}
|