Why does EK9 reject my isSet check as redundant?

← Code Quality · Ref: Q559

EK9 tracks whether each variable is set, unset, or unknown at every program point. When the compiler can prove the outcome of an isSet check, it flags it as redundant.

REDUNDANT ISSET CHECK (E08088)

If a variable is known to be set, checking it with ? is pointless. Examples: a variable assigned from a literal ('name <- "hello"; if name?'), a variable inside a guard block ('if name <- getName(); if name?' — the guard already proved it is set), or a variable assigned from a constructor ('list <- List(); if list?').

ALWAYS-FALSE ISSET CHECK (E08089)

If the compiler already knows a variable is unset, checking it with ? is always false. Example: 'other <- getName(); if other? ... else if other?' — in the else branch the first check has already proved 'other' is unset, so the second check can never succeed. A variable declared with '?' and never assigned is a different error: using it at all is E08020 (might be used before being initialised).

WHEN ISSET IS GENUINELY NEEDED

The ? operator is needed when the variable might or might not be set depending on runtime conditions. A value from a function call may be unset. A value from a Dict lookup may be absent. A value conditionally assigned in only one branch of an if statement needs checking afterward.

GUARD PATTERN REPLACES ISSET

Instead of 'result <- lookup(key); if result?', use the guard pattern: 'if result <- lookup(key)'. The guard combines the call and the isSet check into one expression, and the variable is only in scope when it is known to be set.

A GUARD'S OWN CHECK COUNTS TOO

A guard's gate is an isSet check you do not write as '?', so the same two errors apply to it: 'if seven <- 7' always enters (E08088) and 'if value <- Integer()' never does (E08089) - a literal type's no-argument constructor is unset. Re-testing the guard's own variable in its condition is always redundant: in 'if name <- fetchName() then name?' the condition only runs once the guard has found name set (E08088). Over a Boolean the guard enters only when the value is set AND true, so 'if ok <- check() then ok' repeats it (E08086). And an 'x?' that is one operand of an 'and' is judged like a whole condition: 'if x := 7 then x? and ready' is E08088. 'if x := fetch() then x?' - re-point an existing variable and enter only when the new value is set - stays legal, because nothing proves fetch() set.

See Q557 for flow-sensitive dead code detection. See Q558 for tautological condition patterns. See Q311 for the full quality checks catalog. See Q640 for collection isSet check patterns.

Example

defines module qa.codequality.issetcheck

  defines function

    fetchName()
      <- rtn as String: "Alice"

    fetchOptionalTitle()
      <- rtn as String: String()

    <?-
      Correct: isSet check on a value from a function call is genuinely useful
      because the function might return an unset value.
    -?>
    greetPerson()
      -> name as String
      <- greeting as String: `Hello ${name}`

      title <- fetchOptionalTitle()
      if title?
        greeting: `Hello ${title} ${name}`

    <?-
      Correct: the guard pattern combines lookup and isSet check.
      The variable is only in scope when it is known to be set.
    -?>
    greetWithGuard()
      <- greeting as String: "Hello stranger"

      if name <- fetchName()
        greeting: `Hello ${name}`

    <?-
      Correct: isSet check on a value from another function call
      where the result might be unset. The compiler does not know
      the runtime return value, so the check is genuinely needed.
    -?>
    buildFullGreeting()
      -> name as String
      <- result as String: `Greeting for ${name}`

      optionalSuffix <- fetchOptionalTitle()
      if optionalSuffix?
        result: `Dear ${optionalSuffix} ${name}`

  defines program

    RedundantIsSetDemo()
      stdout <- Stdout()

      personGreeting <- greetPerson("Bob")
      stdout.println(personGreeting)

      guardGreeting <- greetWithGuard()
      stdout.println(guardGreeting)

      fullGreeting <- buildFullGreeting("Carol")
      stdout.println(fullGreeting)

Common mistakes

E08090 — Declaring a variable and never using it is dead code. Every variable must be referenced. See ek9 -h E08090 for details.

Incorrect:

unused <- greetPerson("Bob")

Correct:

personGreeting <- greetPerson("Bob")
      stdout.println(personGreeting)

E08088 — The guard already checked name: its 'then' condition runs only once the guard has found name set, so 'then name?' is always true. Drop it. See ek9 -h E08088 for details.

Incorrect:

if name <- fetchName() then name?

Correct:

if name <- fetchName()
Other ways to ask this
  • What is E08088 redundant isSet check?
  • What is E08089 never-set isSet check?
  • How does EK9 track set and unset variables?
  • When is the ? operator genuinely needed?

Coming from another language?

Java: no compiler tracking of null vs non-null at each program point. Optional.isPresent() is never flagged as redundant. Kotlin: smart casts track nullability but do not flag redundant null checks. Rust: Option matching is exhaustive but redundant Some checks are not flagged. Go: no nil tracking. Python: no None tracking. C++: no nullptr flow analysis. EK9: mandatory flow-sensitive tracking of set/unset state with compiler errors for redundant or always-failing checks.

Keywords: set, variable, guard, null-safe, E08088, E08089, isSet, unset, safe, quality, check, tracking, redundant, clean-code, flow, isset, metric