|
| 1 | +//! Regression tests for the setPrototypeOf cycle-walk chain-end bug |
| 2 | +//! (regression introduced by 1f4a2c5bd / #5763, bisected via the Next.js |
| 3 | +//! boot failure). |
| 4 | +//! |
| 5 | +//! The OrdinarySetPrototypeOf cycle check uses Floyd's tortoise-and-hare. |
| 6 | +//! Two chain-end bugs made `Object.setPrototypeOf(obj, proto)` throw |
| 7 | +//! "Cannot convert undefined or null to object" on perfectly ordinary, |
| 8 | +//! acyclic prototype chains: |
| 9 | +//! |
| 10 | +//! 1. Only the hare's SECOND step was null-guarded. On any acyclic chain |
| 11 | +//! longer than one link (every function/class proto: |
| 12 | +//! fn -> Function.prototype -> Object.prototype -> null) the hare reached |
| 13 | +//! null first and the next iteration called advance(null) -> |
| 14 | +//! js_object_get_prototype_of(null) -> TypeError. comment-json's |
| 15 | +//! `__extends` hit this on every transpiled subclass, killing Next.js |
| 16 | +//! server boot. |
| 17 | +//! |
| 18 | +//! 2. `js_object_get_prototype_of` returns undefined (not spec's |
| 19 | +//! object-or-null) for some exotic receivers; the walk fed that back into |
| 20 | +//! the next advance and threw. comment-json's `__extends` feature-test |
| 21 | +//! `{ __proto__: [] }` hit this at boot. Undefined can never be part of a |
| 22 | +//! genuine cycle, so it must end the walk like null. |
| 23 | +//! |
| 24 | +//! All expected outputs are byte-for-byte what `node |
| 25 | +//! --experimental-strip-types` prints. |
| 26 | +
|
| 27 | +use std::path::PathBuf; |
| 28 | +use std::process::Command; |
| 29 | + |
| 30 | +fn perry_bin() -> PathBuf { |
| 31 | + PathBuf::from(env!("CARGO_BIN_EXE_perry")) |
| 32 | +} |
| 33 | + |
| 34 | +fn compile_and_run(dir: &std::path::Path, source: &str) -> String { |
| 35 | + let entry = dir.join("main.ts"); |
| 36 | + let output = dir.join("main_bin"); |
| 37 | + std::fs::write(&entry, source).expect("write entry"); |
| 38 | + |
| 39 | + let compile = Command::new(perry_bin()) |
| 40 | + .current_dir(dir) |
| 41 | + .arg("compile") |
| 42 | + .arg(&entry) |
| 43 | + .arg("-o") |
| 44 | + .arg(&output) |
| 45 | + .arg("--no-cache") |
| 46 | + .output() |
| 47 | + .expect("run perry compile"); |
| 48 | + assert!( |
| 49 | + compile.status.success(), |
| 50 | + "perry compile failed\nstdout:\n{}\nstderr:\n{}", |
| 51 | + String::from_utf8_lossy(&compile.stdout), |
| 52 | + String::from_utf8_lossy(&compile.stderr) |
| 53 | + ); |
| 54 | + |
| 55 | + let run = Command::new(&output) |
| 56 | + .current_dir(dir) |
| 57 | + .output() |
| 58 | + .expect("run compiled binary"); |
| 59 | + assert!( |
| 60 | + run.status.success(), |
| 61 | + "compiled binary failed (exit {:?})\nstdout:\n{}\nstderr:\n{}", |
| 62 | + run.status.code(), |
| 63 | + String::from_utf8_lossy(&run.stdout), |
| 64 | + String::from_utf8_lossy(&run.stderr) |
| 65 | + ); |
| 66 | + String::from_utf8_lossy(&run.stdout).into_owned() |
| 67 | +} |
| 68 | + |
| 69 | +/// Bug 1 guard: setPrototypeOf onto acyclic chains of length 2 and 4 must |
| 70 | +/// succeed and establish inheritance. Pre-fix the hare walked past the chain |
| 71 | +/// end and every one of these threw "Cannot convert undefined or null to |
| 72 | +/// object". |
| 73 | +#[test] |
| 74 | +fn set_prototype_of_acyclic_chain_longer_than_one_link() { |
| 75 | + let dir = tempfile::tempdir().expect("tempdir"); |
| 76 | + let stdout = compile_and_run( |
| 77 | + dir.path(), |
| 78 | + r#" |
| 79 | +const base = { |
| 80 | + greet() { |
| 81 | + return "hi"; |
| 82 | + }, |
| 83 | +}; |
| 84 | +
|
| 85 | +// Chain length 2: base -> Object.prototype -> null. |
| 86 | +const obj: any = {}; |
| 87 | +Object.setPrototypeOf(obj, base); |
| 88 | +console.log(obj.greet()); |
| 89 | +
|
| 90 | +// Chain length 4: deep2 -> deep1 -> base -> Object.prototype -> null. |
| 91 | +const deep1 = Object.create(base); |
| 92 | +const deep2 = Object.create(deep1); |
| 93 | +const target: any = {}; |
| 94 | +Object.setPrototypeOf(target, deep2); |
| 95 | +console.log(target.greet()); |
| 96 | +console.log("ok"); |
| 97 | +"#, |
| 98 | + ); |
| 99 | + assert_eq!( |
| 100 | + stdout, "hi\nhi\nok\n", |
| 101 | + "setPrototypeOf onto an acyclic chain of length >= 2 must not throw \ |
| 102 | + (hare must freeze at chain end)" |
| 103 | + ); |
| 104 | +} |
| 105 | + |
| 106 | +/// The `__extends` statics link that killed Next.js boot: setPrototypeOf of |
| 107 | +/// one function onto another. The proto chain of a function is |
| 108 | +/// fn -> Function.prototype -> Object.prototype -> null (three links), so |
| 109 | +/// pre-fix this threw on every transpiled subclass. |
| 110 | +#[test] |
| 111 | +fn set_prototype_of_function_statics_link() { |
| 112 | + let dir = tempfile::tempdir().expect("tempdir"); |
| 113 | + let stdout = compile_and_run( |
| 114 | + dir.path(), |
| 115 | + r#" |
| 116 | +function Base() {} |
| 117 | +function Derived() {} |
| 118 | +Object.setPrototypeOf(Derived, Base); |
| 119 | +console.log(Object.getPrototypeOf(Derived) === Base); |
| 120 | +console.log("statics linked"); |
| 121 | +"#, |
| 122 | + ); |
| 123 | + assert_eq!( |
| 124 | + stdout, "true\nstatics linked\n", |
| 125 | + "setPrototypeOf(fn, fn) — a 3-link proto chain — must not throw" |
| 126 | + ); |
| 127 | +} |
| 128 | + |
| 129 | +/// Bug 2 guard: comment-json's `__extends` feature test. The object-literal |
| 130 | +/// `{ __proto__: [] }` routes through the same cycle walk with an exotic |
| 131 | +/// receiver whose getPrototypeOf reports undefined mid-walk; undefined must |
| 132 | +/// terminate the walk like null instead of being fed back into the next |
| 133 | +/// advance. Pre-fix, merely EVALUATING the literal threw and killed boot. |
| 134 | +/// |
| 135 | +/// NOTE: this deliberately does not assert `probe instanceof Array` — that |
| 136 | +/// is `true` in node but still `false` in perry (a separate, pre-existing |
| 137 | +/// instanceof/literal-`__proto__` gap unchanged by this fix). This test |
| 138 | +/// guards only the throw. |
| 139 | +#[test] |
| 140 | +fn object_literal_proto_array_feature_test() { |
| 141 | + let dir = tempfile::tempdir().expect("tempdir"); |
| 142 | + let stdout = compile_and_run( |
| 143 | + dir.path(), |
| 144 | + r#" |
| 145 | +const probe: any = { __proto__: [] }; |
| 146 | +console.log(typeof probe); |
| 147 | +console.log("feature test ok"); |
| 148 | +"#, |
| 149 | + ); |
| 150 | + assert_eq!( |
| 151 | + stdout, "object\nfeature test ok\n", |
| 152 | + "evaluating {{ __proto__: [] }} must not throw \ |
| 153 | + (undefined getPrototypeOf ends the walk)" |
| 154 | + ); |
| 155 | +} |
| 156 | + |
| 157 | +/// The cycle check itself must keep working: setting a prototype that would |
| 158 | +/// form a cycle still throws a TypeError, and the object stays usable. |
| 159 | +#[test] |
| 160 | +fn cycle_detection_still_throws() { |
| 161 | + let dir = tempfile::tempdir().expect("tempdir"); |
| 162 | + let stdout = compile_and_run( |
| 163 | + dir.path(), |
| 164 | + r#" |
| 165 | +const a: any = {}; |
| 166 | +const b: any = {}; |
| 167 | +Object.setPrototypeOf(b, a); |
| 168 | +let threw = false; |
| 169 | +try { |
| 170 | + Object.setPrototypeOf(a, b); |
| 171 | +} catch (e) { |
| 172 | + threw = true; |
| 173 | +} |
| 174 | +console.log("cycle detected:", threw); |
| 175 | +"#, |
| 176 | + ); |
| 177 | + assert_eq!( |
| 178 | + stdout, "cycle detected: true\n", |
| 179 | + "a genuine prototype cycle must still be rejected with a TypeError" |
| 180 | + ); |
| 181 | +} |
0 commit comments