cfsolve 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/README.md +10 -9
  4. data/UPSTREAM.md +6 -6
  5. data/lib/cfsolve/profiles/safari-26-ios/answers.json +2562 -1887
  6. data/lib/cfsolve/profiles/safari-26-ios/challenge-page-answers.json +1873 -1198
  7. data/lib/cfsolve/profiles/safari-26-ios.json +0 -8
  8. data/lib/cfsolve/version.rb +2 -2
  9. data/vendor/cfsolve/README.md +0 -35
  10. data/vendor/cfsolve/examples/build_profile.rs +17 -15
  11. data/vendor/cfsolve/examples/capture_session.rs +37 -0
  12. data/vendor/cfsolve/examples/check_answers.rs +66 -0
  13. data/vendor/cfsolve/examples/compare_session.rs +190 -0
  14. data/vendor/cfsolve/examples/stage2_snapshot.rs +4 -1
  15. data/vendor/cfsolve/profiles/safari-26-ios/answers.json +2562 -1887
  16. data/vendor/cfsolve/profiles/safari-26-ios/challenge-page-answers.json +1873 -1198
  17. data/vendor/cfsolve/profiles/safari-26-ios.json +0 -8
  18. data/vendor/cfsolve/scripts/capture_cssom.mjs +42 -0
  19. data/vendor/cfsolve/scripts/serve_cssom.py +111 -0
  20. data/vendor/cfsolve/src/capture/profile.rs +7 -18
  21. data/vendor/cfsolve/src/capture/recorded.rs +42 -7
  22. data/vendor/cfsolve/src/challenge/installed/audit.rs +131 -0
  23. data/vendor/cfsolve/src/challenge/installed.rs +122 -271
  24. data/vendor/cfsolve/src/challenge/payload.rs +107 -5
  25. data/vendor/cfsolve/src/challenge/state.rs +98 -16
  26. data/vendor/cfsolve/src/deobfuscate/encoded_strings.rs +3 -2
  27. data/vendor/cfsolve/src/deobfuscate/helper_objects.rs +17 -32
  28. data/vendor/cfsolve/src/deobfuscate/register_arrays.rs +18 -5
  29. data/vendor/cfsolve/src/embedding_page/extra_params.rs +0 -14
  30. data/vendor/cfsolve/src/embedding_page/page.rs +28 -2
  31. data/vendor/cfsolve/src/http/client.rs +11 -0
  32. data/vendor/cfsolve/src/js/ast.rs +12 -3
  33. data/vendor/cfsolve/src/js/objects.rs +9 -2
  34. data/vendor/cfsolve/src/listing/fold.rs +0 -83
  35. data/vendor/cfsolve/src/listing/query.rs +25 -244
  36. data/vendor/cfsolve/src/listing.rs +0 -1
  37. data/vendor/cfsolve/src/session/initial.rs +14 -5
  38. data/vendor/cfsolve/src/session/probes.rs +160 -125
  39. data/vendor/cfsolve/src/session/timing.rs +58 -53
  40. data/vendor/cfsolve/src/session/widget.rs +5 -6
  41. data/vendor/cfsolve/src/stage2/analysis/analyze.rs +50 -33
  42. data/vendor/cfsolve/src/stage2/analysis/discovery.rs +61 -66
  43. data/vendor/cfsolve/src/stage2/analysis/envelope.rs +31 -21
  44. data/vendor/cfsolve/src/stage2/analysis/recognize.rs +3 -5
  45. data/vendor/cfsolve/src/stage2/analysis.rs +0 -1
  46. data/vendor/cfsolve/src/stage2/audit_globals.rs +310 -0
  47. data/vendor/cfsolve/src/stage2/benchmark.rs +5 -8
  48. data/vendor/cfsolve/src/stage2/call_stack.rs +72 -16
  49. data/vendor/cfsolve/src/stage2/interaction/native.rs +2 -1
  50. data/vendor/cfsolve/src/stage2/interaction/plan.rs +0 -3
  51. data/vendor/cfsolve/src/stage2/payload/recover.rs +54 -23
  52. data/vendor/cfsolve/src/stage2/probes/ice.rs +11 -10
  53. data/vendor/cfsolve/src/stage2/probes/media.rs +47 -33
  54. data/vendor/cfsolve/src/stage2/probes/performance.rs +1 -0
  55. data/vendor/cfsolve/src/stage2/probes/rects.rs +12 -17
  56. data/vendor/cfsolve/src/stage2/probes/stack.rs +50 -103
  57. data/vendor/cfsolve/src/stage2/probes/websocket_echo.rs +17 -86
  58. data/vendor/cfsolve/src/stage2/probes.rs +0 -1
  59. data/vendor/cfsolve/src/stage2/records.rs +4 -13
  60. data/vendor/cfsolve/src/stage2/reference.json +1 -1
  61. data/vendor/cfsolve/src/stage2/reference.rs +98 -100
  62. data/vendor/cfsolve/src/stage2/registry.rs +41 -34
  63. data/vendor/cfsolve/src/stage2/replay.rs +182 -254
  64. data/vendor/cfsolve/src/stage2/stylesheets.rs +176 -0
  65. data/vendor/cfsolve/src/stage2/trace.rs +18 -0
  66. data/vendor/cfsolve/src/stage2/vocabulary.rs +111 -401
  67. data/vendor/cfsolve/src/stage2.rs +2 -1
  68. data/vendor/cfsolve/src/vm/disassemble.rs +39 -10
  69. data/vendor/cfsolve/src/vm/interpret/access.rs +5 -21
  70. data/vendor/cfsolve/src/vm/interpret/builtin.rs +6 -53
  71. data/vendor/cfsolve/src/vm/interpret/call.rs +86 -9
  72. data/vendor/cfsolve/src/vm/interpret/compile.rs +626 -0
  73. data/vendor/cfsolve/src/vm/interpret/copies.rs +232 -120
  74. data/vendor/cfsolve/src/vm/interpret/evaluate/tests.rs +96 -8
  75. data/vendor/cfsolve/src/vm/interpret/evaluate.rs +219 -92
  76. data/vendor/cfsolve/src/vm/interpret/explore.rs +155 -39
  77. data/vendor/cfsolve/src/vm/interpret/frozen.rs +171 -0
  78. data/vendor/cfsolve/src/vm/interpret/intrinsic.rs +350 -0
  79. data/vendor/cfsolve/src/vm/interpret/plan.rs +71 -44
  80. data/vendor/cfsolve/src/vm/interpret/run.rs +202 -13
  81. data/vendor/cfsolve/src/vm/interpret.rs +7 -2
  82. data/vendor/cfsolve/src/vm/machine.rs +6 -2
  83. data/vendor/cfsolve/src/vm/packed_names.rs +307 -0
  84. data/vendor/cfsolve/src/vm/session.rs +36 -14
  85. data/vendor/cfsolve/src/vm/value.rs +13 -0
  86. data/vendor/cfsolve/src/vm.rs +1 -0
  87. data/vendor/cfsolve/vendor/btls-sys/src/lib.rs +3 -0
  88. metadata +13 -6
  89. data/vendor/cfsolve/src/listing/normalize.rs +0 -264
  90. data/vendor/cfsolve/src/stage2/analysis/outputs.rs +0 -327
  91. data/vendor/cfsolve/src/stage2/probes/stylesheets.rs +0 -249
  92. data/vendor/cfsolve/src/stage2/reference-expanded.json +0 -42
  93. data/vendor/cfsolve/src/stage2/session_names.rs +0 -470
@@ -1,470 +0,0 @@
1
- //! Refreshes program-specific names and namespace classifications in captured device facts.
2
-
3
- use std::collections::{BTreeMap, BTreeSet};
4
-
5
- use anyhow::{Context, Result, ensure};
6
- use serde::{Deserialize, Serialize};
7
- use swc_core::ecma::{
8
- ast::{
9
- AssignExpr, AssignTarget, BinaryOp, Expr, IfStmt, Lit, Pat, SimpleAssignTarget, Stmt,
10
- VarDeclarator,
11
- },
12
- visit::{Visit, VisitWith},
13
- };
14
- use url::Url;
15
-
16
- use super::{
17
- records::{Probe, ProbePlan},
18
- reference::ENVIRONMENT_AUDIT_GROUPS,
19
- trace::Trace,
20
- vocabulary::Vocabulary,
21
- };
22
- use crate::{
23
- challenge::{installed::Installed, program::PreparedSession},
24
- js::{JsObject, JsValue, ast},
25
- script::Source,
26
- };
27
-
28
- type Groups = BTreeMap<String, Vec<String>>;
29
-
30
- #[derive(Default)]
31
- struct Namespace {
32
- roots: BTreeSet<String>,
33
- writes: Vec<(String, String, String)>,
34
- }
35
-
36
- fn is_root(value: &Expr) -> bool {
37
- let Expr::Bin(expression) = value.unwrap_parens() else {
38
- return false;
39
- };
40
- expression.op == BinaryOp::LogicalOr
41
- && matches!(expression.left.unwrap_parens(), Expr::This(_))
42
- && matches!(expression.right.unwrap_parens(), Expr::Ident(name) if name.sym == *"self")
43
- }
44
-
45
- impl Visit for Namespace {
46
- fn visit_var_declarator(&mut self, declaration: &VarDeclarator) {
47
- if let (Pat::Ident(binding), Some(value)) = (&declaration.name, &declaration.init)
48
- && is_root(value)
49
- {
50
- self.roots.insert(binding.id.sym.to_string());
51
- }
52
- declaration.visit_children_with(self);
53
- }
54
-
55
- fn visit_assign_expr(&mut self, assignment: &AssignExpr) {
56
- if let AssignTarget::Simple(SimpleAssignTarget::Ident(binding)) = &assignment.left
57
- && is_root(&assignment.right)
58
- {
59
- self.roots.insert(binding.id.sym.to_string());
60
- }
61
- if let AssignTarget::Simple(SimpleAssignTarget::Member(member)) = &assignment.left
62
- && let (Expr::Ident(owner), Some(name)) =
63
- (member.obj.unwrap_parens(), ast::member_str(&member.prop))
64
- {
65
- let kind = match assignment.right.unwrap_parens() {
66
- Expr::Fn(_) => "function".into(),
67
- Expr::Object(_) => "object".into(),
68
- Expr::Array(_) => "array".into(),
69
- Expr::Lit(Lit::Bool(v)) => v.value.to_string(),
70
- Expr::Lit(Lit::Num(v)) => v.value.to_string(),
71
- _ => "other".into(),
72
- };
73
- self.writes.push((owner.sym.to_string(), name.into(), kind));
74
- }
75
- assignment.visit_children_with(self);
76
- }
77
- }
78
-
79
- fn source_names(source: &Source) -> Groups {
80
- let mut ns = Namespace::default();
81
- source.parsed().script.visit_with(&mut ns);
82
- let mut groups: BTreeMap<String, Vec<String>> = BTreeMap::new();
83
- for (root, name, kind) in ns.writes {
84
- if ns.roots.contains(&root) {
85
- let group = groups.entry(format!("source:{kind}")).or_default();
86
- if !group.contains(&name) {
87
- group.push(name);
88
- }
89
- }
90
- }
91
- groups
92
- }
93
-
94
- struct Evidence {
95
- groups: Groups,
96
- markers: BTreeMap<String, String>,
97
- functions: Vec<String>,
98
- }
99
-
100
- fn names(source: &Source, installed: &Installed) -> Evidence {
101
- let mut groups = source_names(source);
102
- let mut functions = groups.remove("source:function").unwrap_or_default();
103
- functions.extend(installed.factories.iter().cloned());
104
- functions.extend(
105
- installed
106
- .audit
107
- .iter()
108
- .filter(|(name, kind)| kind.as_str() == "N" && !installed.window.contains_key(*name))
109
- .map(|(name, _)| name.clone()),
110
- );
111
- let mut markers = BTreeMap::new();
112
- for (name, kind) in &installed.audit {
113
- let kind = if installed
114
- .window
115
- .get(name)
116
- .and_then(JsValue::as_str)
117
- .is_some()
118
- {
119
- "string"
120
- } else {
121
- kind.as_str()
122
- };
123
- if kind == "N" {
124
- continue;
125
- }
126
- groups
127
- .entry(format!("installed:{kind}"))
128
- .or_default()
129
- .push(name.clone());
130
- if kind == "string" {
131
- markers.insert(name.clone(), installed.audit[name].clone());
132
- }
133
- }
134
- Evidence {
135
- groups,
136
- markers,
137
- functions,
138
- }
139
- }
140
-
141
- /// The audit gives the iframe's Array constructor a build-specific class marker.
142
- fn array_class(source: &Source) -> Option<String> {
143
- #[derive(Default)]
144
- struct Classes(BTreeSet<String>);
145
- impl Visit for Classes {
146
- fn visit_if_stmt(&mut self, statement: &IfStmt) {
147
- if let Expr::Bin(test) = statement.test.unwrap_parens()
148
- && matches!(test.op, BinaryOp::EqEq | BinaryOp::EqEqEq)
149
- && [&test.left, &test.right].iter().any(|operand| {
150
- matches!(operand.unwrap_parens(), Expr::Member(member) if ast::member_str(&member.prop) == Some("Array"))
151
- })
152
- {
153
- let consequent = match &*statement.cons {
154
- Stmt::Block(block) if block.stmts.len() == 1 => &block.stmts[0],
155
- consequent => consequent,
156
- };
157
- if let Stmt::Return(returned) = consequent
158
- && let Some(value) = &returned.arg
159
- && let Expr::Call(call) = value.unwrap_parens()
160
- && let Some(Expr::Member(callee)) = call.callee.as_expr().map(|value| value.unwrap_parens())
161
- && matches!(callee.obj.unwrap_parens(), Expr::Ident(name) if name.sym == *"String")
162
- && ast::member_str(&callee.prop) == Some("fromCharCode")
163
- && let [argument] = call.args.as_slice()
164
- && let Expr::Lit(Lit::Num(number)) = argument.expr.unwrap_parens()
165
- && number.value.fract() == 0.0
166
- && (1.0..=127.0).contains(&number.value)
167
- {
168
- self.0.insert((number.value as u8 as char).to_string());
169
- }
170
- }
171
- statement.visit_children_with(self);
172
- }
173
- }
174
- let mut classes = Classes::default();
175
- source.parsed().script.visit_with(&mut classes);
176
- (classes.0.len() == 1).then(|| classes.0.into_iter().next().unwrap())
177
- }
178
-
179
- /// Captured program strings and namespace roles used to refresh session-dependent values.
180
- #[derive(Clone, Debug, Deserialize, Serialize)]
181
- pub struct CapturedNames {
182
- features: Vec<Vec<String>>,
183
- globals: Groups,
184
- markers: BTreeMap<String, String>,
185
- }
186
-
187
- impl CapturedNames {
188
- pub(crate) fn capture(source: &Source, plan: &ProbePlan) -> Result<Self> {
189
- let prepared =
190
- PreparedSession::try_new(source, &Url::parse("https://challenges.cloudflare.com/")?)?;
191
- let Evidence {
192
- groups: globals,
193
- markers,
194
- ..
195
- } = names(source, &prepared.program().installed);
196
- Ok(Self {
197
- features: plan
198
- .probes()
199
- .iter()
200
- .map(|p| p.expanded_features.clone())
201
- .collect(),
202
- globals,
203
- markers,
204
- })
205
- }
206
- }
207
-
208
- pub(crate) struct Refresh {
209
- direct: Vocabulary,
210
- aliases: BTreeMap<String, String>,
211
- functions: Vec<String>,
212
- array_class: Option<String>,
213
- trace: Option<Trace>,
214
- controller: Option<String>,
215
- }
216
-
217
- impl Refresh {
218
- pub(crate) fn new(
219
- captured: &CapturedNames,
220
- source: &Source,
221
- plan: &ProbePlan,
222
- installed: &Installed,
223
- ) -> Result<Self> {
224
- let current: Vec<_> = plan
225
- .probes()
226
- .iter()
227
- .map(|p| p.expanded_features.as_slice())
228
- .collect();
229
- let direct = Vocabulary::between(&current, &captured.features);
230
- let Evidence {
231
- groups: new_names,
232
- markers: new_markers,
233
- functions,
234
- } = names(source, installed);
235
- let mut aliases = BTreeMap::new();
236
- for (kind, old) in &captured.globals {
237
- // Functions are rebuilt from the current program, so neither their order nor
238
- // their count needs to match the captured build.
239
- if matches!(kind.as_str(), "source:function" | "installed:N") {
240
- continue;
241
- }
242
- let new = new_names
243
- .get(kind)
244
- .with_context(|| format!("missing namespace category {kind}"))?;
245
- ensure!(
246
- old.len() == new.len(),
247
- "namespace category {kind} changed count"
248
- );
249
- for (old, new) in old.iter().zip(new) {
250
- aliases.insert(old.clone(), new.clone());
251
- if let (Some(before), Some(after)) =
252
- (captured.markers.get(old), new_markers.get(new))
253
- {
254
- aliases.insert(before.clone(), after.clone());
255
- }
256
- }
257
- }
258
- Ok(Self {
259
- direct,
260
- aliases,
261
- functions,
262
- array_class: array_class(source),
263
- trace: Trace::recover(source),
264
- controller: plan.controller_trace.clone(),
265
- })
266
- }
267
- pub(crate) fn apply(
268
- &self,
269
- probe: &Probe,
270
- vocabulary: &Vocabulary,
271
- fields: &mut JsObject,
272
- ) -> Result<()> {
273
- self.direct.rewrite_to_session(probe.position(), fields);
274
- super::vocabulary::rewrite_object(fields, &|value| {
275
- if let Some(new) = self.aliases.get(value) {
276
- *value = new.clone();
277
- } else if let Some(name) = value
278
- .strip_prefix("o.")
279
- .and_then(|name| self.aliases.get(name))
280
- {
281
- *value = format!("o.{name}");
282
- }
283
- });
284
- let key = vocabulary.to_session(probe.position(), ENVIRONMENT_AUDIT_GROUPS);
285
- if let Some(groups) = fields.get_mut(key).and_then(JsValue::as_object_mut) {
286
- if let Some(functions) = groups.get_mut("N").and_then(JsValue::as_array_mut) {
287
- functions.retain(|name| {
288
- !name
289
- .as_str()
290
- .is_some_and(|name| name.starts_with("o.") && name != "o.Array")
291
- });
292
- functions.extend(
293
- self.functions
294
- .iter()
295
- .map(|name| JsValue::String(format!("o.{name}"))),
296
- );
297
- }
298
- if let Some(class) = &self.array_class {
299
- move_member(groups, "Array".into(), class)?;
300
- }
301
- let trace = self
302
- .trace
303
- .as_ref()
304
- .context("the current program has no namespace trace")?;
305
- super::replay::refresh_trace(
306
- trace,
307
- groups,
308
- &probe.expanded_features,
309
- self.controller.as_deref(),
310
- )?;
311
- }
312
- Ok(())
313
- }
314
- }
315
-
316
- /// Moves a captured member into the class recovered from the current program.
317
- fn move_member(groups: &mut JsObject, member: String, class: &str) -> Result<()> {
318
- let member = JsValue::String(member);
319
- let mut found = false;
320
- for members in groups.values_mut() {
321
- let members = members
322
- .as_array_mut()
323
- .context("an environment audit group is not an array")?;
324
- found |= members.contains(&member);
325
- members.retain(|name| name != &member);
326
- }
327
- groups.retain(|_, members| {
328
- members
329
- .as_array()
330
- .is_some_and(|members| !members.is_empty())
331
- });
332
- if found {
333
- groups
334
- .entry(class.to_owned())
335
- .or_insert_with(|| JsValue::Array(vec![]))
336
- .as_array_mut()
337
- .expect("audit groups were validated")
338
- .push(member);
339
- }
340
- Ok(())
341
- }
342
-
343
- #[cfg(test)]
344
- mod tests {
345
- use super::*;
346
-
347
- #[test]
348
- fn namespace_refresh_uses_the_current_trace_with_or_without_a_controller() {
349
- let probe = Probe {
350
- index: 1,
351
- token: "test".into(),
352
- input: JsValue::Null,
353
- operation: None,
354
- features: vec![],
355
- expanded_features: ["entry", "replaceTrace"].map(str::to_owned).to_vec(),
356
- writes: Default::default(),
357
- };
358
- let vocabulary = Vocabulary::infer::<&[String]>(&[]);
359
- for controller in [None, Some("controller".to_owned())] {
360
- let refresh = Refresh {
361
- direct: vocabulary.clone(),
362
- aliases: BTreeMap::new(),
363
- functions: vec![],
364
- array_class: None,
365
- trace: Some(Trace {
366
- global: "trace".into(),
367
- replace: "replaceTrace".into(),
368
- push: None,
369
- initial_root: "initial".into(),
370
- returned_root: "callback".into(),
371
- }),
372
- controller: controller.clone(),
373
- };
374
- let mut fields = JsObject::from_iter([(
375
- ENVIRONMENT_AUDIT_GROUPS.into(),
376
- crate::js::json!({
377
- "obsolete-captured-trace": ["o.trace", "other"],
378
- }),
379
- )]);
380
- refresh.apply(&probe, &vocabulary, &mut fields).unwrap();
381
- let current = format!(
382
- "initial,{},entry",
383
- controller.as_deref().unwrap_or("callback")
384
- );
385
- assert_eq!(
386
- fields[ENVIRONMENT_AUDIT_GROUPS][&current],
387
- crate::js::json!(["o.trace"])
388
- );
389
- assert_eq!(
390
- fields[ENVIRONMENT_AUDIT_GROUPS]["obsolete-captured-trace"],
391
- crate::js::json!(["other"])
392
- );
393
- }
394
- }
395
-
396
- #[test]
397
- fn rebuilds_functions_when_their_count_changes_without_positional_aliases() {
398
- let source = Source::try_new("var root = this || self; root.a = function() {}; root.b = function() {}; root.state = {};").unwrap();
399
- let mut installed = Installed::default();
400
- installed.audit.insert("marker".into(), "N".into());
401
- installed
402
- .window
403
- .insert("marker".into(), JsValue::String("N".into()));
404
- let evidence = names(&source, &installed);
405
- assert_eq!(
406
- evidence.functions,
407
- ["a", "b"],
408
- "a string marker must not become a function"
409
- );
410
- let plan = crate::js::from_value(crate::js::json!({
411
- "record_fields":{"counter":"counter", "token":"token", "input":"input",
412
- "status":"status", "message":"message", "started":"started",
413
- "finished":"finished", "duration":"duration"},
414
- "payload":{"template":{"fields":{}}, "published":[], "footer":{}, "pending":null},
415
- "probes":[]
416
- }))
417
- .unwrap();
418
- let captured = CapturedNames {
419
- features: vec![],
420
- globals: BTreeMap::from([
421
- ("source:function".into(), vec!["old_function".into()]),
422
- ("installed:N".into(), vec!["old_factory".into()]),
423
- ("source:object".into(), vec!["old_state".into()]),
424
- ]),
425
- markers: BTreeMap::new(),
426
- };
427
- let refreshed = Refresh::new(&captured, &source, &plan, &installed).unwrap();
428
- assert_eq!(refreshed.functions, ["a", "b"]);
429
- assert_eq!(
430
- refreshed.aliases,
431
- BTreeMap::from([("old_state".into(), "state".into())])
432
- );
433
- }
434
-
435
- #[test]
436
- fn reclassification_preserves_other_members_and_merges_classes() {
437
- let mut groups = JsObject::from_iter([
438
- ("old".into(), crate::js::json!(["Array", "other"])),
439
- ("current".into(), crate::js::json!(["existing"])),
440
- ]);
441
- move_member(&mut groups, "Array".into(), "current").unwrap();
442
- assert_eq!(groups["old"], crate::js::json!(["other"]));
443
- assert_eq!(groups["current"], crate::js::json!(["existing", "Array"]));
444
- move_member(&mut groups, "missing".into(), "unused").unwrap();
445
- assert!(!groups.contains_key("unused"));
446
- groups.insert("malformed".into(), JsValue::Null);
447
- assert!(move_member(&mut groups, "Array".into(), "old").is_err());
448
- }
449
-
450
- #[test]
451
- fn array_class_comes_from_the_constructor_check() {
452
- let source = Source::try_new("function classify(frame, value) { if (value === frame.Array) { return String.fromCharCode(68); } if (value === frame.Function) { return String.fromCharCode(67); } }").unwrap();
453
- assert_eq!(array_class(&source).as_deref(), Some("D"));
454
- let ambiguous = Source::try_new("function classify(frame, value) { if (value === frame.Array) return String.fromCharCode(68); if (frame.Array === value) return String.fromCharCode(67); }").unwrap();
455
- assert_eq!(array_class(&ambiguous), None);
456
- let unrelated =
457
- Source::try_new("function unrelated() { return String.fromCharCode(68); }").unwrap();
458
- assert_eq!(array_class(&unrelated), None);
459
- }
460
-
461
- #[test]
462
- fn namespace_roles_follow_assignments_and_ignore_unrelated_helper_objects() {
463
- let source=Source::try_new("var root = this || self; var helper = {}; helper.unrelated = function() {}; root.first = function() {}; root.second = function() {}; root.state = {}; root.enabled = true; var other; other = this || self; other.third = function() {};").unwrap();
464
- let groups = source_names(&source);
465
- assert_eq!(groups["source:function"], vec!["first", "second", "third"]);
466
- assert_eq!(groups["source:object"], vec!["state"]);
467
- assert_eq!(groups["source:true"], vec!["enabled"]);
468
- assert_eq!(groups.len(), 3);
469
- }
470
- }