use super::*;
#[test]
fn test_nil_narrowing_then_branch() {
let errs = errors(
r"pipeline t(task) {
fn greet(name: string | nil) {
if name != nil {
let s: string = name
}
}
}",
);
assert!(errs.is_empty(), "got: {errs:?}");
}
#[test]
fn test_nil_narrowing_else_branch() {
let errs = errors(
r"pipeline t(task) {
fn check(x: string | nil) {
if x != nil {
let s: string = x
} else {
let n: nil = x
}
}
}",
);
assert!(errs.is_empty(), "got: {errs:?}");
}
#[test]
fn test_nil_equality_narrows_both() {
let errs = errors(
r"pipeline t(task) {
fn check(x: string | nil) {
if x == nil {
let n: nil = x
} else {
let s: string = x
}
}
}",
);
assert!(errs.is_empty(), "got: {errs:?}");
}
#[test]
fn test_truthiness_narrowing() {
let errs = errors(
r"pipeline t(task) {
fn check(x: string | nil) {
if x {
let s: string = x
}
}
}",
);
assert!(errs.is_empty(), "got: {errs:?}");
}
#[test]
fn test_negation_narrowing() {
let errs = errors(
r"pipeline t(task) {
fn check(x: string | nil) {
if !x {
let n: nil = x
} else {
let s: string = x
}
}
}",
);
assert!(errs.is_empty(), "got: {errs:?}");
}
#[test]
fn test_typeof_narrowing() {
let errs = errors(
r#"pipeline t(task) {
fn check(x: string | int) {
if type_of(x) == "string" {
let s: string = x
}
}
}"#,
);
assert!(errs.is_empty(), "got: {errs:?}");
}
#[test]
fn test_typeof_narrowing_else() {
let errs = errors(
r#"pipeline t(task) {
fn check(x: string | int) {
if type_of(x) == "string" {
let s: string = x
} else {
let i: int = x
}
}
}"#,
);
assert!(errs.is_empty(), "got: {errs:?}");
}
#[test]
fn test_typeof_neq_narrowing() {
let errs = errors(
r#"pipeline t(task) {
fn check(x: string | int) {
if type_of(x) != "string" {
let i: int = x
} else {
let s: string = x
}
}
}"#,
);
assert!(errs.is_empty(), "got: {errs:?}");
}
#[test]
fn test_and_combines_narrowing() {
let errs = errors(
r#"pipeline t(task) {
fn check(x: string | int | nil) {
if x != nil && type_of(x) == "string" {
let s: string = x
}
}
}"#,
);
assert!(errs.is_empty(), "got: {errs:?}");
}
#[test]
fn test_short_circuit_and_narrows_rhs_expression() {
let errs = errors(
r"pipeline t(task) {
fn count_values(values: list<int>) -> int { return len(values) }
fn check(values: list<int> | nil) {
if values != nil && count_values(values) > 0 {
let present: list<int> = values
}
}
}",
);
assert!(errs.is_empty(), "got: {errs:?}");
}
#[test]
fn test_short_circuit_or_narrows_rhs_expression() {
let errs = errors(
r#"pipeline t(task) {
fn count_values(values: list<int>) -> int { return len(values) }
fn check(values: list<int> | nil) {
if values == nil || count_values(values) > 0 {
log("ok")
}
}
}"#,
);
assert!(errs.is_empty(), "got: {errs:?}");
}
#[test]
fn test_nil_coalescing_call_site_strips_optional_chain_nil() {
let errs = errors(
r"pipeline t(task) {
type Doc = { components: { schemas: dict<int>? }? }
let doc: Doc = { components: nil }
let n: int = len(doc.components?.schemas ?? {})
}",
);
assert!(errs.is_empty(), "got: {errs:?}");
}
#[test]
fn test_or_falsy_narrowing() {
let errs = errors(
r"pipeline t(task) {
fn check(x: string | nil, y: int | nil) {
if x || y {
// conservative: can't narrow
} else {
let xn: nil = x
let yn: nil = y
}
}
}",
);
assert!(errs.is_empty(), "got: {errs:?}");
}
#[test]
fn test_guard_narrows_outer_scope() {
let errs = errors(
r"pipeline t(task) {
fn check(x: string | nil) {
guard x != nil else { return }
let s: string = x
}
}",
);
assert!(errs.is_empty(), "got: {errs:?}");
}
#[test]
fn test_while_narrows_body() {
let errs = errors(
r"pipeline t(task) {
fn check(x: string | nil) {
while x != nil {
let s: string = x
break
}
}
}",
);
assert!(errs.is_empty(), "got: {errs:?}");
}
#[test]
fn test_early_return_narrows_after_if() {
let errs = errors(
r#"pipeline t(task) {
fn check(x: string | nil) -> string {
if x == nil {
return "default"
}
let s: string = x
return s
}
}"#,
);
assert!(errs.is_empty(), "got: {errs:?}");
}
#[test]
fn test_early_throw_narrows_after_if() {
let errs = errors(
r#"pipeline t(task) {
fn check(x: string | nil) {
if x == nil {
throw "missing"
}
let s: string = x
}
}"#,
);
assert!(errs.is_empty(), "got: {errs:?}");
}
#[test]
fn test_no_narrowing_unknown_type() {
let errs = errors(
r"pipeline t(task) {
fn check(x) {
if x != nil {
let s: string = x
}
}
}",
);
assert!(errs.is_empty(), "got: {errs:?}");
}
#[test]
fn test_reassignment_invalidates_narrowing() {
let errs = errors(
r"pipeline t(task) {
fn check(x: string | nil) {
var y: string | nil = x
if y != nil {
let s: string = y
y = nil
let s2: string = y
}
}
}",
);
assert_eq!(errs.len(), 1, "expected 1 error, got: {errs:?}");
assert!(
errs[0].contains("expected string"),
"expected type mismatch, got: {}",
errs[0]
);
}
#[test]
fn test_let_immutable_warning() {
let all = check_source(
r"pipeline t(task) {
let x = 42
x = 43
}",
);
let warnings: Vec<_> = all
.iter()
.filter(|d| d.severity == DiagnosticSeverity::Warning)
.collect();
assert!(
warnings.iter().any(|w| w.message.contains("immutable")),
"expected immutability warning, got: {warnings:?}"
);
}
#[test]
fn test_nested_narrowing() {
let errs = errors(
r#"pipeline t(task) {
fn check(x: string | int | nil) {
if x != nil {
if type_of(x) == "int" {
let i: int = x
}
}
}
}"#,
);
assert!(errs.is_empty(), "got: {errs:?}");
}
#[test]
fn test_match_narrows_arms() {
let errs = errors(
r#"pipeline t(task) {
fn check(x: string | int) {
match x {
"hello" -> {
let s: string = x
}
42 -> {
let i: int = x
}
_ -> {}
}
}
}"#,
);
assert!(errs.is_empty(), "got: {errs:?}");
}
#[test]
fn test_match_discriminator_narrows_kind_tag() {
let errs = errors(
r#"type Msg = {kind: "ping", ttl: int} | {kind: "pong", latency_ms: int}
pipeline t(task) {
fn handle(m: Msg) {
match m.kind {
"ping" -> {
let p: {kind: "ping", ttl: int} = m
}
"pong" -> {
let p: {kind: "pong", latency_ms: int} = m
}
}
}
}"#,
);
assert!(
errs.is_empty(),
"expected narrowing on m.kind, got: {errs:?}"
);
}
#[test]
fn test_match_discriminator_narrows_type_tag() {
let errs = errors(
r#"type Event = {type: "click", x: int, y: int} | {type: "scroll", dy: int}
pipeline t(task) {
fn handle(e: Event) {
match e.type {
"click" -> {
let c: {type: "click", x: int, y: int} = e
}
"scroll" -> {
let s: {type: "scroll", dy: int} = e
}
}
}
}"#,
);
assert!(
errs.is_empty(),
"expected narrowing on e.type, got: {errs:?}"
);
}
#[test]
fn test_match_discriminator_narrows_arbitrary_tag() {
let errs = errors(
r#"type Instr = {op: "add", lhs: int, rhs: int} | {op: "neg", arg: int}
pipeline t(task) {
fn handle(i: Instr) {
match i.op {
"add" -> {
let a: {op: "add", lhs: int, rhs: int} = i
}
"neg" -> {
let n: {op: "neg", arg: int} = i
}
}
}
}"#,
);
assert!(errs.is_empty(), "expected narrowing on i.op, got: {errs:?}");
}
#[test]
fn test_if_discriminator_narrows_kind_then_branch() {
let errs = errors(
r#"type Msg = {kind: "ping", ttl: int} | {kind: "pong", latency_ms: int}
pipeline t(task) {
fn handle(m: Msg) {
if m.kind == "ping" {
let p: {kind: "ping", ttl: int} = m
}
}
}"#,
);
assert!(
errs.is_empty(),
"expected narrowing in then-branch, got: {errs:?}"
);
}
#[test]
fn test_if_discriminator_narrows_else_branch_residual() {
let errs = errors(
r#"type Msg = {kind: "ping", ttl: int} | {kind: "pong", latency_ms: int}
pipeline t(task) {
fn handle(m: Msg) {
if m.kind == "ping" {
let p: {kind: "ping", ttl: int} = m
} else {
let p: {kind: "pong", latency_ms: int} = m
}
}
}"#,
);
assert!(
errs.is_empty(),
"expected narrowing in both branches, got: {errs:?}"
);
}
#[test]
fn test_if_discriminator_neq_inverts_narrowing() {
let errs = errors(
r#"type Msg = {kind: "ping", ttl: int} | {kind: "pong", latency_ms: int}
pipeline t(task) {
fn handle(m: Msg) {
if m.kind != "ping" {
let p: {kind: "pong", latency_ms: int} = m
} else {
let p: {kind: "ping", ttl: int} = m
}
}
}"#,
);
assert!(
errs.is_empty(),
"expected `!=` to invert truthy/falsy, got: {errs:?}"
);
}
#[test]
fn test_discriminator_narrowing_skipped_when_field_unknown() {
let errs = errors(
r#"type Msg = {kind: "ping", ttl: int} | {kind: "pong", latency_ms: int}
pipeline t(task) {
fn handle(m: Msg) {
if m.kind == "ping" {
// Sanity: once narrowed, this assignment to the OTHER variant must fail.
let wrong: {kind: "pong", latency_ms: int} = m
}
}
}"#,
);
assert!(
errs.iter().any(|e| e.contains("let binding `wrong`")),
"expected residual-narrowing assignment to fail, got: {errs:?}"
);
}
#[test]
fn test_has_narrows_optional_field() {
let errs = errors(
r#"pipeline t(task) {
fn check(x: {name?: string, age: int}) {
if x.has("name") {
let n: {name: string, age: int} = x
}
}
}"#,
);
assert!(errs.is_empty(), "got: {errs:?}");
}
#[test]
fn test_match_or_pattern_narrows_to_union_of_variants() {
let errs = errors(
r#"type Msg =
{kind: "ping", ttl: int} |
{kind: "pong", latency_ms: int} |
{kind: "close", reason: string}
pipeline t(task) {
fn handle(m: Msg) -> string {
return match m.kind {
"ping" | "pong" -> {
// Both kinds carry `kind` — access is fine.
let k: string = m.kind
"live"
}
"close" -> { m.reason }
}
}
}"#,
);
assert!(errs.is_empty(), "got: {errs:?}");
}
#[test]
fn test_match_narrows_through_named_alias_member() {
let errs = errors(
r#"type Ping = {kind: "ping", ttl: int}
type Msg = Ping | {kind: "pong", latency_ms: int}
pipeline t(task) {
fn handle(m: Msg) -> string {
return match m.kind {
"ping" -> {
let p: {kind: "ping", ttl: int} = m
"p"
}
"pong" -> { "o" }
}
}
}"#,
);
assert!(errs.is_empty(), "got: {errs:?}");
}
#[test]
fn test_if_narrows_through_named_alias_member() {
let errs = errors(
r#"type Ping = {kind: "ping", ttl: int}
type Msg = Ping | {kind: "pong", latency_ms: int}
pipeline t(task) {
fn handle(m: Msg) -> string {
if m.kind == "ping" {
let p: {kind: "ping", ttl: int} = m
return "p"
}
return "o"
}
}"#,
);
assert!(errs.is_empty(), "got: {errs:?}");
}
#[test]
fn test_match_or_pattern_on_literal_union_narrows_to_sub_union() {
let errs = errors(
r#"pipeline t(task) {
fn sign(v: "pos" | "neg" | "zero") -> string {
return match v {
"pos" | "neg" -> {
let rest: "pos" | "neg" = v
rest
}
"zero" -> { v }
}
}
}"#,
);
assert!(errs.is_empty(), "got: {errs:?}");
}
#[test]
fn test_schema_is_shape_narrowing_preserves_current_fields() {
let errs = errors(
r"type Tag = {b: string}
pipeline t(task) {
fn check(x: {a: int, b: string}) {
if schema_is(x, Tag) {
let _a: int = x.a
let _b: string = x.b
}
}
}",
);
assert!(errs.is_empty(), "got: {errs:?}");
}
#[test]
fn test_schema_is_shape_narrowing_adds_schema_only_required_field() {
let errs = errors(
r"type WithTag = {kind: string}
pipeline t(task) {
fn check(x: {a: int}) {
if schema_is(x, WithTag) {
let _a: int = x.a
let _kind: string = x.kind
}
}
}",
);
assert!(errs.is_empty(), "got: {errs:?}");
}
#[test]
fn test_schema_is_narrows_iter_union() {
let errs = errors(
r"type IterInt = iter<int>
pipeline t(task) {
fn check(x: iter<int> | string) {
if schema_is(x, IterInt) {
let _i: iter<int> = x
}
}
}",
);
assert!(errs.is_empty(), "got: {errs:?}");
}
#[test]
fn test_schema_is_narrows_owned_union_and_preserves_marker() {
let errs = errors(
r"type Ch = channel
pipeline t(task) {
fn check(x: owned<channel> | nil) {
if schema_is(x, Ch) {
let _c: owned<channel> = x
drop(_c)
}
}
}",
);
assert!(errs.is_empty(), "got: {errs:?}");
}
#[test]
fn test_vacuous_condition_lint_schema_is_always_true_width_subtype() {
let warns = warnings(
r"type Tag = {b: string}
pipeline t(task) {
fn check(x: {a: int, b: string}) {
if schema_is(x, Tag) {
let _b: string = x.b
}
}
}",
);
assert!(
warns.iter().any(|w| w.contains("always true")),
"expected always-true warning, got: {warns:?}"
);
}
#[test]
fn test_vacuous_condition_lint_schema_is_always_false_disjoint() {
let warns = warnings(
r"type Tag = {kind: string}
pipeline t(task) {
fn check(x: int) {
if schema_is(x, Tag) {
log(x)
}
}
}",
);
assert!(
warns.iter().any(|w| w.contains("always false")),
"expected always-false warning, got: {warns:?}"
);
}
#[test]
fn test_vacuous_condition_lint_silent_on_unknown() {
let warns = warnings(
r#"type Tag = {b: string}
pipeline t(task) {
fn check(x: unknown) {
if schema_is(x, Tag) {
log("ok")
}
}
}"#,
);
assert!(
!warns
.iter()
.any(|w| w.contains("always true") || w.contains("always false")),
"expected no vacuous-condition warning on unknown var, got: {warns:?}"
);
}
#[test]
fn test_vacuous_condition_lint_respects_optional_field() {
let warns = warnings(
r"type Tag = {b: string}
pipeline t(task) {
fn check(x: {b: string?}) {
if schema_is(x, Tag) {
let _b: string = x.b
}
}
}",
);
assert!(
!warns.iter().any(|w| w.contains("always true")),
"optional-vs-required mismatch must not fire always-true, got: {warns:?}"
);
}
#[test]
fn test_vacuous_condition_lint_skips_bare_true_literal() {
let warns = warnings(
r#"pipeline t(task) {
if true {
log("always runs")
}
}"#,
);
assert!(
!warns.iter().any(|w| w.contains("always truthy")),
"bare `if true` is a block-scope idiom and must not fire, got: {warns:?}"
);
}
#[test]
fn test_vacuous_condition_lint_skips_bare_false_literal() {
let warns = warnings(
r#"pipeline t(task) {
if false {
log("never runs")
}
}"#,
);
assert!(
!warns.iter().any(|w| w.contains("always falsy")),
"bare `if false` is a disable-block idiom and must not fire, got: {warns:?}"
);
}
#[test]
fn test_vacuous_condition_lint_constant_or_short_circuit() {
let warns = warnings(
r#"pipeline t(task) {
fn check(x: int) {
if true || x > 0 {
log("always runs")
}
}
}"#,
);
assert!(
warns.iter().any(|w| w.contains("always truthy")),
"expected `true || _` short-circuit warning, got: {warns:?}"
);
}
#[test]
fn test_vacuous_condition_lint_constant_and_short_circuit() {
let warns = warnings(
r#"pipeline t(task) {
fn check(x: int) {
if x > 0 && false {
log("never runs")
}
}
}"#,
);
assert!(
warns.iter().any(|w| w.contains("always falsy")),
"expected `_ && false` short-circuit warning, got: {warns:?}"
);
}
#[test]
fn test_vacuous_condition_lint_negated_constant() {
let warns = warnings(
r#"pipeline t(task) {
if !true {
log("never runs")
}
}"#,
);
assert!(
warns.iter().any(|w| w.contains("always falsy")),
"expected `!true` warning, got: {warns:?}"
);
}
#[test]
fn test_vacuous_condition_lint_silent_on_normal_narrowing() {
let warns = warnings(
r#"type A = {kind: "a"}
pipeline t(task) {
fn check(x: {kind: "a", extra: int} | {kind: "b"}) {
if schema_is(x, A) {
log(x)
}
}
}"#,
);
assert!(
!warns
.iter()
.any(|w| w.contains("always true") || w.contains("always false")),
"real narrowing must stay silent, got: {warns:?}"
);
}
#[test]
fn test_vacuous_condition_lint_fires_for_is_type_alias() {
let warns = warnings(
r"type Tag = {b: string}
pipeline t(task) {
fn check(x: {a: int, b: string}) {
if is_type(x, Tag) {
let _b: string = x.b
}
}
}",
);
assert!(
warns
.iter()
.any(|w| w.contains("is_type") && w.contains("always true")),
"expected `is_type` always-true warning, got: {warns:?}"
);
}
#[test]
fn test_vacuous_condition_lint_descends_through_negation() {
let warns = warnings(
r"type Tag = {b: string}
pipeline t(task) {
fn check(x: {a: int, b: string}) {
if !schema_is(x, Tag) {
log(x)
}
}
}",
);
assert!(
warns
.iter()
.any(|w| w.contains("schema_is") && w.contains("always true")),
"expected `schema_is` always-true through negation, got: {warns:?}"
);
}
#[test]
fn test_vacuous_condition_lint_fires_in_while_condition() {
let warns = warnings(
r#"pipeline t(task) {
fn check(cond: bool) {
while cond || true {
log("forever")
break
}
}
}"#,
);
assert!(
warns.iter().any(|w| w.contains("always truthy")),
"expected compound-folded `while` warning, got: {warns:?}"
);
}
#[test]
fn test_vacuous_condition_lint_fires_in_guard_condition() {
let warns = warnings(
r"pipeline t(task) {
fn check(cond: bool) -> int {
guard cond && false else { return 0 }
return 1
}
}",
);
assert!(
warns.iter().any(|w| w.contains("always falsy")),
"expected compound-folded `guard` warning, got: {warns:?}"
);
}
#[test]
fn test_vacuous_condition_lint_sees_nil_refined_scope_for_rhs() {
let warns = warnings(
r"type Tag = {b: string}
pipeline t(task) {
fn check(x: {a: int, b: string} | nil) {
if x != nil && schema_is(x, Tag) {
let _b: string = x.b
}
}
}",
);
assert!(
warns
.iter()
.any(|w| w.contains("schema_is") && w.contains("always true")),
"expected schema_is to be vacuous after `x != nil` narrowing, got: {warns:?}"
);
}