Skip to main content

ferrijs_std/node/
deep_equal.rs

1//! Structural equality, in Node's two flavours.
2//!
3//! One implementation, shared by `util.isDeepStrictEqual` and by
4//! `assert.deepEqual` / `assert.deepStrictEqual`.
5
6use rquickjs::{Function, Object, Type, Value, function::This};
7
8/// How leaf values compare.
9#[derive(Clone, Copy, PartialEq, Eq)]
10pub enum Mode {
11  /// `assert.deepEqual`: `==` on primitives, prototypes ignored.
12  Loose,
13  /// `assert.deepStrictEqual`: `Object.is` on primitives, prototypes must
14  /// match.
15  Strict,
16}
17
18/// Recursion ceiling. A cyclic graph would otherwise never terminate;
19/// Node tracks visited pairs instead, which needs identity keys QuickJS
20/// does not hand out cheaply.
21const MAX_DEPTH: usize = 64;
22
23/// Compare two values the way `assert.deepEqual` / `deepStrictEqual` do.
24///
25/// # Errors
26///
27/// Propagates JS-side property reads.
28pub fn deep_equal<'js>(a: &Value<'js>, b: &Value<'js>, mode: Mode) -> rquickjs::Result<bool> {
29  equal_at(a, b, mode, 0)
30}
31
32/// `===`: `Object.is` on primitives (so `NaN` matches itself and `0` does
33/// not match `-0`), identity on everything else.
34#[must_use]
35pub fn strict_equal<'js>(a: &Value<'js>, b: &Value<'js>) -> bool {
36  if a.type_of() != b.type_of() {
37    return false;
38  }
39  match a.type_of() {
40    Type::Int | Type::Float => number_eq(a, b, Mode::Strict),
41    Type::String => a
42      .as_string()
43      .and_then(|s| s.to_string().ok())
44      .eq(&b.as_string().and_then(|s| s.to_string().ok())),
45    Type::Bool => a.as_bool() == b.as_bool(),
46    Type::Undefined | Type::Null | Type::Uninitialized => true,
47    _ => a == b,
48  }
49}
50
51/// `==`: strict equality, plus the primitive coercions.
52#[must_use]
53pub fn loose_equal<'js>(a: &Value<'js>, b: &Value<'js>) -> bool {
54  if a.type_of() == b.type_of() {
55    // `==` and `===` differ only across types, except that `NaN` is equal
56    // to nothing under either.
57    return match a.type_of() {
58      Type::Int | Type::Float => number_eq(a, b, Mode::Loose),
59      _ => strict_equal(a, b),
60    };
61  }
62  loose_primitive_eq(a, b)
63}
64
65fn equal_at<'js>(a: &Value<'js>, b: &Value<'js>, mode: Mode, depth: usize) -> rquickjs::Result<bool> {
66  if depth > MAX_DEPTH {
67    return Ok(false);
68  }
69  if a.type_of() != b.type_of() {
70    // `1 == '1'` under the loose flavour; nothing else crosses types.
71    return Ok(mode == Mode::Loose && loose_primitive_eq(a, b));
72  }
73
74  match a.type_of() {
75    Type::Undefined | Type::Null | Type::Uninitialized => Ok(true),
76    Type::Bool => Ok(a.as_bool() == b.as_bool()),
77    Type::Int | Type::Float => Ok(number_eq(a, b, mode)),
78    Type::String => Ok(js_string(a)? == js_string(b)?),
79    Type::Symbol => Ok(a.as_symbol() == b.as_symbol()),
80    Type::Array => array_eq(a, b, mode, depth),
81    Type::Object | Type::Exception => object_eq(a, b, mode, depth),
82    // Functions, constructors and everything else compare by identity.
83    _ => Ok(a == b),
84  }
85}
86
87fn js_string(value: &Value<'_>) -> rquickjs::Result<String> {
88  value.as_string().map_or_else(|| Ok(String::new()), rquickjs::String::to_string)
89}
90
91/// `Object.is` semantics under Strict (so `NaN` equals itself and `0` does
92/// not equal `-0`), `==` semantics under Loose.
93fn number_eq(a: &Value<'_>, b: &Value<'_>, mode: Mode) -> bool {
94  let (Some(x), Some(y)) = (a.as_number(), b.as_number()) else {
95    return false;
96  };
97  match mode {
98    Mode::Strict => {
99      if x.is_nan() && y.is_nan() {
100        true
101      } else {
102        x == y && x.is_sign_negative() == y.is_sign_negative()
103      }
104    },
105    Mode::Loose => x == y,
106  }
107}
108
109/// `==` across types: the numeric coercion JS performs for
110/// number/string/boolean pairs, plus `null == undefined`. Objects are NOT
111/// coerced through `valueOf` / `toString` — Node's `assert.equal` on an
112/// object against a primitive is a comparison nobody writes on purpose.
113fn loose_primitive_eq(a: &Value<'_>, b: &Value<'_>) -> bool {
114  let nullish = |v: &Value<'_>| v.is_null() || v.is_undefined();
115  if nullish(a) || nullish(b) {
116    return nullish(a) && nullish(b);
117  }
118  match (coerce_number(a), coerce_number(b)) {
119    (Some(x), Some(y)) => x == y,
120    _ => false,
121  }
122}
123
124/// `ToNumber` for the primitive types `==` coerces.
125fn coerce_number(value: &Value<'_>) -> Option<f64> {
126  match value.type_of() {
127    Type::Int | Type::Float => value.as_number(),
128    Type::Bool => value.as_bool().map(|b| if b { 1.0 } else { 0.0 }),
129    Type::String => {
130      let text = value.as_string()?.to_string().ok()?;
131      let trimmed = text.trim();
132      if trimmed.is_empty() {
133        return Some(0.0);
134      }
135      trimmed.parse::<f64>().ok()
136    },
137    _ => None,
138  }
139}
140
141fn array_eq<'js>(a: &Value<'js>, b: &Value<'js>, mode: Mode, depth: usize) -> rquickjs::Result<bool> {
142  let (Some(x), Some(y)) = (a.as_array(), b.as_array()) else {
143    return Ok(false);
144  };
145  if x.len() != y.len() {
146    return Ok(false);
147  }
148  for i in 0..x.len() {
149    let (lhs, rhs): (Value<'_>, Value<'_>) = (x.get(i)?, y.get(i)?);
150    if !equal_at(&lhs, &rhs, mode, depth + 1)? {
151      return Ok(false);
152    }
153  }
154  Ok(true)
155}
156
157fn object_eq<'js>(a: &Value<'js>, b: &Value<'js>, mode: Mode, depth: usize) -> rquickjs::Result<bool> {
158  let (Some(x), Some(y)) = (a.as_object(), b.as_object()) else {
159    return Ok(false);
160  };
161
162  if mode == Mode::Strict && constructor_name(x)? != constructor_name(y)? {
163    return Ok(false);
164  }
165
166  // Dates and RegExps carry their state outside their own properties.
167  if let (Some(lhs), Some(rhs)) = (value_of_number(x)?, value_of_number(y)?) {
168    return Ok(lhs == rhs || (lhs.is_nan() && rhs.is_nan()));
169  }
170  if let (Some(lhs), Some(rhs)) = (regexp_source(x)?, regexp_source(y)?) {
171    return Ok(lhs == rhs);
172  }
173
174  let keys: Vec<String> = own_keys(x)?;
175  let other: Vec<String> = own_keys(y)?;
176  if keys.len() != other.len() {
177    return Ok(false);
178  }
179  for key in keys {
180    if !y.contains_key(key.as_str())? {
181      return Ok(false);
182    }
183    let (lhs, rhs): (Value<'_>, Value<'_>) = (x.get(key.as_str())?, y.get(key.as_str())?);
184    if !equal_at(&lhs, &rhs, mode, depth + 1)? {
185      return Ok(false);
186    }
187  }
188  Ok(true)
189}
190
191fn own_keys(object: &Object<'_>) -> rquickjs::Result<Vec<String>> {
192  object.keys::<String>().collect::<rquickjs::Result<Vec<String>>>()
193}
194
195fn constructor_name(object: &Object<'_>) -> rquickjs::Result<Option<String>> {
196  let ctor: Option<Object<'_>> = object.get("constructor").ok();
197  match ctor {
198    Some(c) => Ok(c.get::<_, String>("name").ok()),
199    None => Ok(None),
200  }
201}
202
203/// `valueOf()` for the wrappers whose identity is a number — `Date`, and the
204/// boxed primitives.
205fn value_of_number(object: &Object<'_>) -> rquickjs::Result<Option<f64>> {
206  if constructor_name(object)?.as_deref() != Some("Date") {
207    return Ok(None);
208  }
209  let value_of: Function<'_> = object.get("valueOf")?;
210  let millis: f64 = value_of.call((This(object.clone()),))?;
211  Ok(Some(millis))
212}
213
214fn regexp_source(object: &Object<'_>) -> rquickjs::Result<Option<String>> {
215  if constructor_name(object)?.as_deref() != Some("RegExp") {
216    return Ok(None);
217  }
218  let source: String = object.get("source")?;
219  let flags: String = object.get("flags").unwrap_or_default();
220  Ok(Some(format!("/{source}/{flags}")))
221}