Skip to content

S15: bool flags or string constants instead of an enum

S15Severity: Medium

States encoded as flags or strings, so invalid combinations are possible.

Clippy: no lint finds this. Look for it in review.

String constants for a status, and three bool parameters for one choice:

pub const STATUS_NEW: &str = "new";
pub const STATUS_PAID: &str = "paid";
pub const STATUS_SHIPPED: &str = "shipped";
pub fn shipping_cents(express: bool, overnight: bool, pickup: bool) -> u64 {
if pickup {
0
} else if overnight {
2_500
} else if express {
1_500
} else {
500
}
}
pub fn can_ship(status: &str) -> bool {
status == STATUS_PAID
}

And the call sites:

let cents = shipping_cents(true, false, true); // which flag is which?
let ok = can_ship("Paid"); // compiles, and is false
  • It copies older Java. public static final String STATUS_PAID = "paid" and boolean express parameters are common in older Java code, and the agent translates them directly.
  • It follows the input. A JSON field "status": "paid" or a database column of type VARCHAR becomes a String in the struct, and stays one.
  • Flags grow one at a time. The first version has express: bool. A later prompt adds overnight: bool, then pickup: bool. Each step is small, and nobody adds the enum.
  • The call site does not say what it means. shipping_cents(true, false, true) needs the definition to read. Swapped flags compile.
  • Invalid combinations compile. Three flags have eight combinations. “Express and pickup” is meaningless, and the function silently picks one (pickup, because of the order of the if chain). The test worse_has_impossible_and_silent_cases shows it.
  • Typos compile. can_ship("Paid") compares with "paid" and returns false. The constants do not help, because the parameter accepts any &str.
  • The compiler cannot help when a case is added. A new status is a new string. Nothing lists the code that must handle it.
  1. WorseThree bool parameters for one choice, and a &str status compared with constants.

    pub const STATUS_NEW: &str = "new";
    pub const STATUS_PAID: &str = "paid";
    pub const STATUS_SHIPPED: &str = "shipped";
    pub fn shipping_cents(express: bool, overnight: bool, pickup: bool) -> u64 {
    if pickup {
    0
    } else if overnight {
    2_500
    } else if express {
    1_500
    } else {
    500
    }
    }
    pub fn can_ship(status: &str) -> bool {
    status == STATUS_PAID
    }
  2. BetterA Delivery enum and a Status enum. shipping_cents is one exhaustive match.

    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    pub enum Delivery {
    Pickup,
    Standard,
    Express,
    Overnight,
    }
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    pub enum Status {
    New,
    Paid,
    Shipped,
    }
    pub fn shipping_cents(delivery: Delivery) -> u64 {
    match delivery {
    Delivery::Pickup => 0,
    Delivery::Standard => 500,
    Delivery::Express => 1_500,
    Delivery::Overnight => 2_500,
    }
    }
    pub fn can_ship(status: Status) -> bool {
    status == Status::Paid
    }

The call sites now say what they mean, and a typo is a compile error:

let cents = shipping_cents(Delivery::Express);
let ok = can_ship(Status::Paid);
  • One bool that is really yes or no, with a clear name: include_archived: bool in a struct field. At a call site, even one bool argument is easier to read as a named field or an enum.
  • Independent flags that can each be on or off in any combination, such as gift_wrap and send_receipt. That is not one choice, so it is not an enum. (Many flags in one struct can still be a sign; Clippy struct_excessive_bools reports more than three.)
  • At the boundary: a string from JSON or a database is fine in the type that is deserialized. Convert it to an enum once, with FromStr or serde, and use the enum inside the program.
  • No Clippy lint reports this pattern in the example. Clippy fn_params_excessive_bools (pedantic) reports functions with more than three bool parameters, and struct_excessive_bools (pedantic) reports structs with more than three bool fields. Both are off by default, and the example has exactly three.
  • Search for const .*: &str = with values that look like states, and for == " comparisons.
  • Look for calls with two or more true or false literals.
Replace the bool parameters that describe one choice with an enum, for
example enum Delivery { Pickup, Standard, Express, Overnight }. Replace the
string status constants with enum Status, and parse strings into it once,
where the data enters the program. Use exhaustive match on the enums, with
no _ arm. Update all callers and run the tests.

Related: S17: Struct with many Option fields that are really states, S16: Wildcard _ => match arm that hides new variants, and Enums and pattern matching.