Skip to content

Commit c19158b

Browse files
committed
Assert eval() behaviour alongside globalEvalWithSourceUrl in tests
The Fantom test now asserts eval()'s result and the source URL attribution difference. The C++ test builds a runtime with RuntimeConfig::EnableEval=false to show eval() throws while globalEvalWithSourceUrl still evaluates source.
1 parent c60a4a1 commit c19158b

3 files changed

Lines changed: 98 additions & 39 deletions

File tree

‎packages/react-native/Libraries/Core/Devtools/__tests__/globalEvalWithSourceUrl-itest.js‎

Lines changed: 55 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,38 +10,71 @@
1010

1111
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
1212

13+
const SOURCE_URL = 'globalEvalWithSourceUrl-itest.bundle';
14+
15+
function getHelper(): (code: string, sourceUrl?: string) => mixed {
16+
// $FlowFixMe[prop-missing]
17+
const helper = global.globalEvalWithSourceUrl;
18+
if (typeof helper !== 'function') {
19+
throw new Error(
20+
`Expected global.globalEvalWithSourceUrl to be a function, got ${typeof helper}`,
21+
);
22+
}
23+
return helper;
24+
}
25+
26+
function getStack(fn: () => mixed): string {
27+
try {
28+
fn();
29+
} catch (e) {
30+
return String(e?.stack ?? '');
31+
}
32+
throw new Error('Expected the evaluated code to throw');
33+
}
34+
1335
describe('globalEvalWithSourceUrl', () => {
14-
it('is installed on the bridgeless runtime', () => {
36+
afterEach(() => {
1537
// $FlowFixMe[prop-missing]
16-
expect(typeof global.globalEvalWithSourceUrl).toBe('function');
38+
delete globalThis.__fantomEvalMarker;
1739
});
1840

19-
it('evaluates source via JSI, documenting how that differs from JS eval', () => {
41+
it('is installed on the bridgeless runtime', () => {
2042
// $FlowFixMe[prop-missing]
21-
const helper = global.globalEvalWithSourceUrl;
22-
expect(typeof helper).toBe('function');
43+
expect(typeof global.globalEvalWithSourceUrl).toBe('function');
44+
});
2345

24-
// Same shape Metro serves for a lazy chunk (source, not bytecode).
46+
it('evaluates the same source as JS eval() in the global scope', () => {
2547
const source = 'globalThis.__fantomEvalMarker = 17; 17';
2648

27-
let evalError: mixed = null;
28-
try {
29-
// eslint-disable-next-line no-eval
30-
eval(source);
31-
} catch (e) {
32-
evalError = e;
33-
}
49+
// eslint-disable-next-line no-eval
50+
expect(eval(source)).toBe(17);
51+
// $FlowFixMe[prop-missing]
52+
expect(globalThis.__fantomEvalMarker).toBe(17);
3453

35-
const helperResult = helper(source, 'globalEvalWithSourceUrl-itest.bundle');
54+
// $FlowFixMe[prop-missing]
55+
delete globalThis.__fantomEvalMarker;
3656

57+
expect(getHelper()(source, SOURCE_URL)).toBe(17);
58+
// $FlowFixMe[prop-missing]
3759
expect(globalThis.__fantomEvalMarker).toBe(17);
38-
expect(helperResult).toBe(17);
39-
40-
if (evalError != null) {
41-
// Lean Hermes: JS eval() is the unsupported path; the helper is JSI.
42-
expect(String(evalError.message || evalError)).toMatch(
43-
/Parsing source code unsupported|eval/i,
44-
);
45-
}
60+
});
61+
62+
it('attributes evaluated code to the given source URL, unlike eval()', () => {
63+
const source = 'throw new Error("thrown from evaluated source")';
64+
65+
// eslint-disable-next-line no-eval
66+
const evalStack = getStack(() => eval(source));
67+
const helperStack = getStack(() => getHelper()(source, SOURCE_URL));
68+
69+
expect(evalStack).not.toContain(SOURCE_URL);
70+
expect(helperStack).toContain(SOURCE_URL);
71+
});
72+
73+
it('rejects an invalid argument count', () => {
74+
const helper = getHelper();
75+
// $FlowFixMe[incompatible-call]
76+
expect(() => helper()).toThrow(
77+
'globalEvalWithSourceUrl arg count must be 1 or 2',
78+
);
4679
});
4780
});

‎packages/react-native/ReactCommon/react/runtime/ReactInstance.cpp‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -458,8 +458,10 @@ void ReactInstance::initializeRuntime(
458458

459459
defineReactInstanceFlags(runtime, options);
460460

461-
// Bridge JSIExecutor installs this so debug loaders can evaluate Metro JS
462-
// via JSI. Hermes does not support JS eval() of Metro `__d(...)` source.
461+
// Bridge JSIExecutor installs this so debug bundle loaders can evaluate
462+
// fetched Metro source through Runtime::evaluateJavaScript, with a source
463+
// URL for stack traces. Unlike JS eval(), that path is not disabled by
464+
// Hermes' RuntimeConfig::EnableEval or by lean engine builds.
463465
defineReadOnlyGlobal(
464466
runtime,
465467
"globalEvalWithSourceUrl",

‎packages/react-native/ReactCommon/react/runtime/tests/cxx/ReactInstanceTest.cpp‎

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,13 @@ class ReactInstanceTest : public ::testing::Test {
118118
protected:
119119
ReactInstanceTest() = default;
120120

121+
virtual ::hermes::vm::RuntimeConfig makeRuntimeConfig() {
122+
return ::hermes::vm::RuntimeConfig();
123+
}
124+
121125
void SetUp() override {
122-
auto runtime =
123-
std::make_unique<JSIRuntimeHolder>(hermes::makeHermesRuntime());
126+
auto runtime = std::make_unique<JSIRuntimeHolder>(
127+
hermes::makeHermesRuntime(makeRuntimeConfig()));
124128
runtime_ = &runtime->getRuntime();
125129
messageQueueThread_ = std::make_shared<MockMessageQueueThread>();
126130
auto mockRegistry = std::make_unique<MockTimerRegistry>();
@@ -256,7 +260,8 @@ TEST_F(ReactInstanceTest, testBridgelessFlagIsSet) {
256260
}
257261

258262
TEST_F(ReactInstanceTest, testGlobalEvalWithSourceUrlIsInstalled) {
259-
auto before = tryEval("typeof globalEvalWithSourceUrl === 'function'", "false");
263+
auto before =
264+
tryEval("typeof globalEvalWithSourceUrl === 'function'", "false");
260265
EXPECT_EQ(before.getBool(), false);
261266
initializeRuntimeWithScript("");
262267
auto isFn = eval("typeof globalEvalWithSourceUrl === 'function'");
@@ -265,21 +270,40 @@ TEST_F(ReactInstanceTest, testGlobalEvalWithSourceUrlIsInstalled) {
265270
EXPECT_EQ(result.getNumber(), 3);
266271
}
267272

268-
TEST_F(ReactInstanceTest, testEvalVersusGlobalEvalWithSourceUrl) {
273+
TEST_F(
274+
ReactInstanceTest,
275+
testGlobalEvalWithSourceUrlMatchesEvalWhenEvalIsEnabled) {
269276
initializeRuntimeWithScript("");
270277

271-
eval("global.__fromHelper = 0; global.__fromEval = 0; global.__evalError = '';");
272-
eval("globalEvalWithSourceUrl('global.__fromHelper = 1', 'chunk.js')");
273-
EXPECT_EQ(eval("global.__fromHelper").getNumber(), 1);
274-
275-
auto evalOk = eval(
276-
"(function(){ try { eval('global.__fromEval = 1'); return true; } catch (e) { global.__evalError = String(e); return false; } })()");
277-
if (evalOk.getBool()) {
278-
EXPECT_EQ(eval("global.__fromEval").getNumber(), 1);
279-
} else {
280-
auto err = eval("global.__evalError");
281-
EXPECT_TRUE(err.isString());
278+
EXPECT_EQ(eval("eval('1 + 2')").getNumber(), 3);
279+
EXPECT_EQ(
280+
eval("globalEvalWithSourceUrl('1 + 2', 'chunk.js')").getNumber(), 3);
281+
}
282+
283+
// Hermes gates eval() and the Function constructor behind
284+
// RuntimeConfig::EnableEval, but Runtime::evaluateJavaScript is not gated.
285+
// globalEvalWithSourceUrl goes through the latter, which is why the debug
286+
// bundle loaders prefer it over eval().
287+
class ReactInstanceWithoutEvalTest : public ReactInstanceTest {
288+
protected:
289+
::hermes::vm::RuntimeConfig makeRuntimeConfig() override {
290+
return ::hermes::vm::RuntimeConfig::Builder().withEnableEval(false).build();
282291
}
292+
};
293+
294+
TEST_F(
295+
ReactInstanceWithoutEvalTest,
296+
testGlobalEvalWithSourceUrlWorksWhenEvalIsDisabled) {
297+
initializeRuntimeWithScript("");
298+
299+
auto evalOutcome = eval(
300+
"(function() { try { eval('1 + 2'); return 'no error'; } catch (e) { return String(e.message); } })()");
301+
EXPECT_THAT(
302+
evalOutcome.getString(*runtime_).utf8(*runtime_),
303+
HasSubstr("Parsing source code unsupported"));
304+
305+
EXPECT_EQ(
306+
eval("globalEvalWithSourceUrl('1 + 2', 'chunk.js')").getNumber(), 3);
283307
}
284308

285309
TEST_F(ReactInstanceTest, testProfilingFlag) {

0 commit comments

Comments
 (0)