Skip to content

Commit ca0d716

Browse files
fix(extractor): inspect executable StyleX loader binding patterns
Refs #690 Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
1 parent 667aa62 commit ca0d716

3 files changed

Lines changed: 194 additions & 0 deletions

File tree

‎libs/extractor/src/scope/stylex_boundary/checks.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ impl<'a> Visit<'a> for Check<'_> {
5151

5252
fn visit_variable_declarator(&mut self, declarator: &VariableDeclarator<'a>) {
5353
if self.required(declarator) {
54+
self.visit_binding_pattern(&declarator.id);
5455
if let Some(Expression::CallExpression(call)) =
5556
declarator.init.as_ref().map(unwrap_syntax_only)
5657
{

‎libs/extractor/src/scope/tests.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ mod stylex;
2020
mod stylex_boundary_coverage;
2121
mod stylex_entrypoint_controls;
2222
mod stylex_entrypoints;
23+
mod stylex_loader_patterns;
2324
mod stylex_residuals;
2425
mod theme_reads;
2526

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
use rstest::rstest;
2+
use serial_test::serial;
3+
4+
use super::extracted;
5+
6+
#[rstest]
7+
#[case("{ [require('@devup-ui/react/compat/stylex')]: value }", "require")]
8+
#[case("{ [import('@devup-ui/react/compat/stylex')]: value }", "import(...)")]
9+
#[case("{ getTheme = require('@devup-ui/react/compat/stylex') }", "require")]
10+
#[case(
11+
"{ getTheme: { missing = require('@devup-ui/react/compat/stylex') } }",
12+
"require"
13+
)]
14+
#[case("[value = require('@devup-ui/react/compat/stylex')]", "require")]
15+
#[case(
16+
"{ getTheme: [value = import('@devup-ui/react/compat/stylex')] }",
17+
"import(...)"
18+
)]
19+
#[case(
20+
"[...{ [require('@devup-ui/react/compat/stylex')]: value }]",
21+
"require"
22+
)]
23+
#[serial]
24+
fn types_only_loader_errors_when_executed_in_an_accepted_binding_pattern(
25+
#[case] pattern: &str,
26+
#[case] loader: &str,
27+
) {
28+
// Given
29+
let declaration = format!("const {pattern} = require('@devup-ui/react');");
30+
let code = format!("const before = 0;\n{declaration}");
31+
let column = declaration
32+
.find("require('@devup-ui/react/compat/stylex')")
33+
.or_else(|| declaration.find("import('@devup-ui/react/compat/stylex')"))
34+
.unwrap_or_else(|| panic!("fixture must contain the inner loader"))
35+
+ 1;
36+
// When
37+
let error = match extracted(&code) {
38+
Err(error) => error,
39+
Ok(output) => panic!("binding-pattern loader must fail: {output}"),
40+
};
41+
// Then
42+
assert_eq!(
43+
error,
44+
format!(
45+
"test.tsx:2:{column}: `StyleX API()` cannot use `{loader}` at build time: compat/stylex has only type exports; use the real stylex entrypoint"
46+
)
47+
);
48+
}
49+
50+
#[rstest]
51+
#[case(
52+
"{ [pt.call(null, { top: 1 })]: value }",
53+
"pt.call",
54+
"invocation/binding member"
55+
)]
56+
#[case(
57+
"{ getTheme = pt.apply(null, [{ top: 1 }]) }",
58+
"pt.apply",
59+
"invocation/binding member"
60+
)]
61+
#[case("{ getTheme = pt.bind(null) }", "pt.bind", "invocation/binding member")]
62+
#[case("{ getTheme = consume(pt) }", "pt", "argument passing")]
63+
#[case("{ getTheme = { api: pt } }", "pt", "object storage")]
64+
#[case("{ getTheme: [value = [pt]] }", "pt", "array storage")]
65+
#[case("[...{ [consume(pt)]: value }]", "pt", "argument passing")]
66+
#[serial]
67+
fn known_api_escape_errors_when_executed_in_an_accepted_binding_pattern(
68+
#[case] pattern: &str,
69+
#[case] site: &str,
70+
#[case] form: &str,
71+
) {
72+
// Given
73+
let declaration = format!("const {pattern} = require('@devup-ui/react');");
74+
let code = format!("import {{ positionTry as pt }} from '@stylexjs/stylex';\n{declaration}");
75+
let column = declaration
76+
.find(site)
77+
.unwrap_or_else(|| panic!("fixture must contain the API occurrence"))
78+
+ 1;
79+
// When
80+
let error = match extracted(&code) {
81+
Err(error) => error,
82+
Ok(output) => panic!("binding-pattern API escape must fail: {output}"),
83+
};
84+
// Then
85+
assert_eq!(
86+
error,
87+
format!(
88+
"test.tsx:2:{column}: `stylex.positionTry()` cannot use `{site}` at build time: unsupported {form}; call the API directly"
89+
)
90+
);
91+
}
92+
93+
#[rstest]
94+
#[case("{ [key()]: value }", "[key()]: value")]
95+
#[case("{ getTheme = fallback }", "getTheme = fallback")]
96+
#[case("{ getTheme: { missing = fallback } }", "missing = fallback")]
97+
#[case("[value = fallback]", "[value = fallback]")]
98+
#[case("[...{ [key()]: value }]", "[key()]: value")]
99+
#[case("{ getTheme, ...rest }", "...rest")]
100+
#[serial]
101+
fn ordinary_binding_patterns_remain_when_the_stylex_gate_is_active(
102+
#[case] pattern: &str,
103+
#[case] retained: &str,
104+
) {
105+
// Given
106+
let code = format!(
107+
"import {{ positionTry as pt }} from '@stylexjs/stylex';\nconst {pattern} = require('@devup-ui/react'); const name = pt({{ top: '1px' }});"
108+
);
109+
// When
110+
let output = extracted(&code)
111+
.unwrap_or_else(|error| panic!("ordinary binding pattern must compile: {error}"));
112+
// Then
113+
assert!(output.contains(retained), "{output}");
114+
assert!(output.contains("require(\"@devup-ui/react\")"), "{output}");
115+
assert!(!output.contains("pt("), "{output}");
116+
}
117+
118+
#[test]
119+
#[serial]
120+
fn mixed_root_pattern_keeps_its_runtime_default_when_stylex_is_consumed() {
121+
// Given
122+
let code = "const { stylex: sx, getTheme = fallback } = require('@devup-ui/react'); const name = sx.positionTry({ top: '1px' });";
123+
// When
124+
let output =
125+
extracted(code).unwrap_or_else(|error| panic!("mixed root pattern must compile: {error}"));
126+
// Then
127+
assert!(output.contains("getTheme = fallback"), "{output}");
128+
assert!(output.contains("require(\"@devup-ui/react\")"), "{output}");
129+
assert!(!output.contains("stylex: sx"), "{output}");
130+
assert!(!output.contains("positionTry("), "{output}");
131+
}
132+
133+
#[rstest]
134+
#[case(
135+
"const sx = require('@devup-ui/react/stylex'); const name = sx.positionTry({ top: '1px' });"
136+
)]
137+
#[case("const { positionTry: pt } = require('@stylexjs/stylex'); const name = pt({ top: '1px' });")]
138+
#[case(
139+
"const root = require('@devup-ui/react'); const name = root.stylex.positionTry({ top: '1px' });"
140+
)]
141+
#[serial]
142+
fn supported_outer_loader_binding_identifiers_are_not_api_reads(#[case] code: &str) {
143+
// Given: a supported direct loader and a consumed API call.
144+
// When
145+
let output = extracted(code)
146+
.unwrap_or_else(|error| panic!("supported outer loader must compile: {error}"));
147+
// Then
148+
assert!(output.contains("const name = \""), "{output}");
149+
assert!(!output.contains("positionTry("), "{output}");
150+
assert!(!output.contains("require("), "{output}");
151+
}
152+
153+
#[rstest]
154+
#[case(
155+
"{ getTheme = ((require) => require('@devup-ui/react/compat/stylex'))(loader) }",
156+
"require(\"@devup-ui/react/compat/stylex\")"
157+
)]
158+
#[case("{ [((pt) => pt.call(null))(fallback)]: value }", "pt.call(null)")]
159+
#[case("{ getTheme = ((pt) => pt.bind(null))(fallback) }", "pt.bind(null)")]
160+
#[serial]
161+
fn shadowed_pattern_loaders_and_functions_remain_when_the_gate_is_active(
162+
#[case] pattern: &str,
163+
#[case] retained: &str,
164+
) {
165+
// Given
166+
let code = format!(
167+
"import {{ positionTry as pt }} from '@stylexjs/stylex';\nconst {pattern} = require('@devup-ui/react'); const name = pt({{ top: '1px' }});"
168+
);
169+
// When
170+
let output = extracted(&code)
171+
.unwrap_or_else(|error| panic!("shadowed pattern expression must compile: {error}"));
172+
// Then
173+
assert!(output.contains(retained), "{output}");
174+
assert!(output.contains("require(\"@devup-ui/react\")"), "{output}");
175+
assert!(output.contains("const name = \""), "{output}");
176+
}
177+
178+
#[test]
179+
#[serial]
180+
fn erased_pattern_types_are_skipped_when_the_gate_is_active() {
181+
// Given
182+
let code = "import { positionTry as pt } from '@stylexjs/stylex';
183+
const { getTheme = fallback }: { getTheme: typeof pt; styles?: import('@devup-ui/react/compat/stylex').PositionTryStyles } = require('@devup-ui/react');
184+
const name = pt({ top: '1px' });";
185+
// When
186+
let output = extracted(code)
187+
.unwrap_or_else(|error| panic!("erased binding types must compile: {error}"));
188+
// Then
189+
assert!(output.contains("getTheme = fallback"), "{output}");
190+
assert!(output.contains("require(\"@devup-ui/react\")"), "{output}");
191+
assert!(output.contains("const name = \""), "{output}");
192+
}

0 commit comments

Comments
 (0)