method-ray 0.1.7 → 0.1.9

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -0
  3. data/{rust → core}/Cargo.toml +1 -1
  4. data/core/src/analyzer/assignments.rs +499 -0
  5. data/{rust → core}/src/analyzer/attributes.rs +2 -1
  6. data/{rust → core}/src/analyzer/blocks.rs +140 -0
  7. data/{rust → core}/src/analyzer/calls.rs +7 -3
  8. data/{rust → core}/src/analyzer/definitions.rs +12 -7
  9. data/{rust → core}/src/analyzer/dispatch.rs +431 -13
  10. data/core/src/analyzer/exceptions.rs +622 -0
  11. data/{rust → core}/src/analyzer/install.rs +37 -1
  12. data/{rust → core}/src/analyzer/literals.rs +3 -17
  13. data/core/src/analyzer/loops.rs +301 -0
  14. data/{rust → core}/src/analyzer/mod.rs +4 -0
  15. data/{rust → core}/src/analyzer/operators.rs +119 -27
  16. data/{rust → core}/src/analyzer/parameters.rs +214 -5
  17. data/core/src/analyzer/super_calls.rs +285 -0
  18. data/{rust → core}/src/cache/rbs_cache.rs +0 -1
  19. data/{rust → core}/src/cli/commands.rs +3 -3
  20. data/{rust → core}/src/diagnostics/diagnostic.rs +0 -3
  21. data/{rust → core}/src/diagnostics/formatter.rs +0 -1
  22. data/{rust → core}/src/env/box_manager.rs +2 -4
  23. data/{rust → core}/src/env/global_env.rs +28 -7
  24. data/{rust → core}/src/env/local_env.rs +35 -1
  25. data/{rust → core}/src/env/method_registry.rs +117 -25
  26. data/{rust → core}/src/env/scope.rs +91 -4
  27. data/{rust → core}/src/env/vertex_manager.rs +0 -1
  28. data/{rust → core}/src/graph/box.rs +134 -8
  29. data/{rust → core}/src/graph/change_set.rs +14 -0
  30. data/{rust → core}/src/lsp/server.rs +1 -1
  31. data/{rust → core}/src/rbs/loader.rs +1 -2
  32. data/{rust → core}/src/source_map.rs +0 -1
  33. data/{rust → core}/src/types.rs +11 -1
  34. data/ext/Cargo.toml +2 -2
  35. data/lib/methodray/binary_locator.rb +2 -2
  36. data/lib/methodray/commands.rb +1 -1
  37. data/lib/methodray/version.rb +1 -1
  38. metadata +54 -50
  39. /data/{rust → core}/src/analyzer/conditionals.rs +0 -0
  40. /data/{rust → core}/src/analyzer/parentheses.rs +0 -0
  41. /data/{rust → core}/src/analyzer/returns.rs +0 -0
  42. /data/{rust → core}/src/analyzer/variables.rs +0 -0
  43. /data/{rust → core}/src/cache/mod.rs +0 -0
  44. /data/{rust → core}/src/checker.rs +0 -0
  45. /data/{rust → core}/src/cli/args.rs +0 -0
  46. /data/{rust → core}/src/cli/mod.rs +0 -0
  47. /data/{rust → core}/src/diagnostics/mod.rs +0 -0
  48. /data/{rust → core}/src/env/mod.rs +0 -0
  49. /data/{rust → core}/src/env/type_error.rs +0 -0
  50. /data/{rust → core}/src/graph/mod.rs +0 -0
  51. /data/{rust → core}/src/graph/vertex.rs +0 -0
  52. /data/{rust → core}/src/lib.rs +0 -0
  53. /data/{rust → core}/src/lsp/diagnostics.rs +0 -0
  54. /data/{rust → core}/src/lsp/main.rs +0 -0
  55. /data/{rust → core}/src/lsp/mod.rs +0 -0
  56. /data/{rust → core}/src/main.rs +0 -0
  57. /data/{rust → core}/src/parser.rs +0 -0
  58. /data/{rust → core}/src/rbs/converter.rs +0 -0
  59. /data/{rust → core}/src/rbs/error.rs +0 -0
  60. /data/{rust → core}/src/rbs/mod.rs +0 -0
@@ -118,13 +118,9 @@ fn install_array_literal_elements(
118
118
 
119
119
  let array_type = if element_types.is_empty() {
120
120
  Type::array()
121
- } else if element_types.len() == 1 {
122
- let elem_type = element_types.into_iter().next().unwrap();
123
- Type::array_of(elem_type)
124
121
  } else {
125
122
  let types_vec: Vec<Type> = element_types.into_iter().collect();
126
- let union_type = Type::Union(types_vec);
127
- Type::array_of(union_type)
123
+ Type::array_of(Type::union_of(types_vec))
128
124
  };
129
125
 
130
126
  Some(genv.new_source(array_type))
@@ -176,18 +172,8 @@ fn install_hash_literal_elements(
176
172
  let hash_type = if key_types.is_empty() || value_types.is_empty() {
177
173
  Type::hash()
178
174
  } else {
179
- let key_type = if key_types.len() == 1 {
180
- key_types.into_iter().next().unwrap()
181
- } else {
182
- let types_vec: Vec<Type> = key_types.into_iter().collect();
183
- Type::Union(types_vec)
184
- };
185
- let value_type = if value_types.len() == 1 {
186
- value_types.into_iter().next().unwrap()
187
- } else {
188
- let types_vec: Vec<Type> = value_types.into_iter().collect();
189
- Type::Union(types_vec)
190
- };
175
+ let key_type = Type::union_of(key_types.into_iter().collect());
176
+ let value_type = Type::union_of(value_types.into_iter().collect());
191
177
  Type::hash_of(key_type, value_type)
192
178
  };
193
179
 
@@ -0,0 +1,301 @@
1
+ //! Loops - while/until loop type inference
2
+ //!
3
+ //! Ruby loop expressions evaluate to nil (except when break passes a value).
4
+ //! Break value propagation is not yet supported.
5
+
6
+ use crate::env::{GlobalEnv, LocalEnv};
7
+ use crate::graph::{ChangeSet, VertexId};
8
+ use crate::types::Type;
9
+ use ruby_prism::{ForNode, UntilNode, WhileNode};
10
+
11
+ use super::bytes_to_name;
12
+ use super::install::{install_node, install_statements};
13
+
14
+ /// Process WhileNode: `while predicate; statements; end`
15
+ ///
16
+ /// Returns nil. Traverses predicate and body to register method calls
17
+ /// and variable assignments in the type graph.
18
+ pub(crate) fn process_while_node(
19
+ genv: &mut GlobalEnv,
20
+ lenv: &mut LocalEnv,
21
+ changes: &mut ChangeSet,
22
+ source: &str,
23
+ while_node: &WhileNode,
24
+ ) -> Option<VertexId> {
25
+ install_node(genv, lenv, changes, source, &while_node.predicate());
26
+
27
+ if let Some(stmts) = while_node.statements() {
28
+ install_statements(genv, lenv, changes, source, &stmts);
29
+ }
30
+
31
+ Some(genv.new_source(Type::Nil))
32
+ }
33
+
34
+ /// Process UntilNode: `until predicate; statements; end`
35
+ ///
36
+ /// Returns nil. Traverses predicate and body to register method calls
37
+ /// and variable assignments in the type graph.
38
+ pub(crate) fn process_until_node(
39
+ genv: &mut GlobalEnv,
40
+ lenv: &mut LocalEnv,
41
+ changes: &mut ChangeSet,
42
+ source: &str,
43
+ until_node: &UntilNode,
44
+ ) -> Option<VertexId> {
45
+ install_node(genv, lenv, changes, source, &until_node.predicate());
46
+
47
+ if let Some(stmts) = until_node.statements() {
48
+ install_statements(genv, lenv, changes, source, &stmts);
49
+ }
50
+
51
+ Some(genv.new_source(Type::Nil))
52
+ }
53
+
54
+ /// Process ForNode: `for index in collection; statements; end`
55
+ ///
56
+ /// Ruby's `for` does NOT create a new scope — the loop variable persists
57
+ /// after the loop. This differs from `collection.each { |x| }` which
58
+ /// creates a block scope.
59
+ ///
60
+ /// Returns nil (consistent with while/until; Ruby's for returns the
61
+ /// collection, but the return value is rarely used in practice).
62
+ pub(crate) fn process_for_node(
63
+ genv: &mut GlobalEnv,
64
+ lenv: &mut LocalEnv,
65
+ changes: &mut ChangeSet,
66
+ source: &str,
67
+ for_node: &ForNode,
68
+ ) -> Option<VertexId> {
69
+ let collection_vtx = install_node(genv, lenv, changes, source, &for_node.collection());
70
+
71
+ // TODO: MultiTargetNode (e.g., `for a, b in [[1, "x"]]`) is not yet supported
72
+ if let Some(target) = for_node.index().as_local_variable_target_node() {
73
+ let name = bytes_to_name(target.name().as_slice());
74
+ let var_vtx = genv.new_vertex();
75
+
76
+ // Array[T] or Range[T] → loop var gets T
77
+ let elem_type = collection_vtx
78
+ .and_then(|vtx| genv.get_source(vtx))
79
+ .and_then(|src| src.ty.type_args())
80
+ .and_then(|args| args.first().cloned());
81
+ if let Some(ty) = elem_type {
82
+ let elem_src = genv.new_source(ty);
83
+ genv.add_edge(elem_src, var_vtx);
84
+ }
85
+
86
+ lenv.new_var(name, var_vtx);
87
+ }
88
+
89
+ if let Some(stmts) = for_node.statements() {
90
+ install_statements(genv, lenv, changes, source, &stmts);
91
+ }
92
+
93
+ Some(genv.new_source(Type::Nil))
94
+ }
95
+
96
+ #[cfg(test)]
97
+ mod tests {
98
+ use crate::analyzer::install::AstInstaller;
99
+ use crate::env::{GlobalEnv, LocalEnv};
100
+ use crate::graph::VertexId;
101
+ use crate::parser::ParseSession;
102
+ use crate::types::Type;
103
+
104
+ /// Helper: parse Ruby source, process with AstInstaller, and return GlobalEnv
105
+ fn analyze(source: &str) -> GlobalEnv {
106
+ let session = ParseSession::new();
107
+ let parse_result = session.parse_source(source, "test.rb").unwrap();
108
+ let root = parse_result.node();
109
+ let program = root.as_program_node().unwrap();
110
+
111
+ let mut genv = GlobalEnv::new();
112
+ let mut lenv = LocalEnv::new();
113
+
114
+ let mut installer = AstInstaller::new(&mut genv, &mut lenv, source);
115
+ for stmt in &program.statements().body() {
116
+ installer.install_node(&stmt);
117
+ }
118
+ installer.finish();
119
+
120
+ genv
121
+ }
122
+
123
+ /// Helper: get the type string for a vertex ID (checks both Vertex and Source)
124
+ fn get_type_show(genv: &GlobalEnv, vtx: VertexId) -> String {
125
+ if let Some(vertex) = genv.get_vertex(vtx) {
126
+ vertex.show()
127
+ } else if let Some(source) = genv.get_source(vtx) {
128
+ source.ty.show()
129
+ } else {
130
+ panic!("vertex {:?} not found as either Vertex or Source", vtx);
131
+ }
132
+ }
133
+
134
+ #[test]
135
+ fn test_while_returns_nil() {
136
+ let source = r#"
137
+ class Foo
138
+ def bar
139
+ while true
140
+ "hello"
141
+ end
142
+ end
143
+ end
144
+ "#;
145
+ let genv = analyze(source);
146
+ let info = genv
147
+ .resolve_method(&Type::instance("Foo"), "bar")
148
+ .expect("Foo#bar should be registered");
149
+ let ret_vtx = info.return_vertex.unwrap();
150
+ assert_eq!(get_type_show(&genv, ret_vtx), "nil");
151
+ }
152
+
153
+ #[test]
154
+ fn test_until_returns_nil() {
155
+ let source = r#"
156
+ class Foo
157
+ def bar
158
+ until false
159
+ "hello"
160
+ end
161
+ end
162
+ end
163
+ "#;
164
+ let genv = analyze(source);
165
+ let info = genv
166
+ .resolve_method(&Type::instance("Foo"), "bar")
167
+ .expect("Foo#bar should be registered");
168
+ let ret_vtx = info.return_vertex.unwrap();
169
+ assert_eq!(get_type_show(&genv, ret_vtx), "nil");
170
+ }
171
+
172
+ #[test]
173
+ fn test_while_variable_assignment_in_body() {
174
+ // Should not panic — variable assignment inside loop is processed
175
+ let source = r#"
176
+ x = "initial"
177
+ while true
178
+ x = "hello"
179
+ end
180
+ "#;
181
+ analyze(source);
182
+ }
183
+
184
+ #[test]
185
+ fn test_while_modifier_form() {
186
+ let source = r#"
187
+ class Foo
188
+ def bar
189
+ x = "hello" while false
190
+ end
191
+ end
192
+ "#;
193
+ let genv = analyze(source);
194
+ let info = genv
195
+ .resolve_method(&Type::instance("Foo"), "bar")
196
+ .expect("Foo#bar should be registered");
197
+ let ret_vtx = info.return_vertex.unwrap();
198
+ assert_eq!(get_type_show(&genv, ret_vtx), "nil");
199
+ }
200
+
201
+ #[test]
202
+ fn test_begin_end_while() {
203
+ let source = r#"
204
+ class Foo
205
+ def bar
206
+ begin
207
+ "hello"
208
+ end while false
209
+ end
210
+ end
211
+ "#;
212
+ let genv = analyze(source);
213
+ let info = genv
214
+ .resolve_method(&Type::instance("Foo"), "bar")
215
+ .expect("Foo#bar should be registered");
216
+ let ret_vtx = info.return_vertex.unwrap();
217
+ assert_eq!(get_type_show(&genv, ret_vtx), "nil");
218
+ }
219
+
220
+ // --- for loop tests ---
221
+
222
+ #[test]
223
+ fn test_for_returns_nil() {
224
+ let source = r#"
225
+ class Foo
226
+ def bar
227
+ for x in [1, 2, 3]
228
+ x
229
+ end
230
+ end
231
+ end
232
+ "#;
233
+ let genv = analyze(source);
234
+ let info = genv
235
+ .resolve_method(&Type::instance("Foo"), "bar")
236
+ .expect("Foo#bar should be registered");
237
+ let ret_vtx = info.return_vertex.unwrap();
238
+ assert_eq!(get_type_show(&genv, ret_vtx), "nil");
239
+ }
240
+
241
+ #[test]
242
+ fn test_for_variable_type_from_array() {
243
+ let source = r#"
244
+ for item in [1, 2, 3]
245
+ item
246
+ end
247
+ "#;
248
+ // Should not panic; loop variable is registered
249
+ analyze(source);
250
+ }
251
+
252
+ #[test]
253
+ fn test_for_variable_persists_after_loop() {
254
+ // for does NOT create a new scope — variable persists
255
+ let source = r#"
256
+ class Foo
257
+ def bar
258
+ for x in [1, 2, 3]
259
+ end
260
+ x
261
+ end
262
+ end
263
+ "#;
264
+ // Should not panic — x is accessible after the loop
265
+ analyze(source);
266
+ }
267
+
268
+ #[test]
269
+ fn test_for_empty_body() {
270
+ let source = r#"
271
+ class Foo
272
+ def bar
273
+ for x in [1, 2, 3]
274
+ end
275
+ end
276
+ end
277
+ "#;
278
+ let genv = analyze(source);
279
+ let info = genv
280
+ .resolve_method(&Type::instance("Foo"), "bar")
281
+ .expect("Foo#bar should be registered");
282
+ let ret_vtx = info.return_vertex.unwrap();
283
+ assert_eq!(get_type_show(&genv, ret_vtx), "nil");
284
+ }
285
+
286
+ #[test]
287
+ fn test_for_with_method_call_in_body() {
288
+ // Should not panic — method call on loop variable is processed
289
+ // (type error check requires RBS, covered by Ruby integration test)
290
+ let source = r#"
291
+ class Foo
292
+ def bar
293
+ for item in ["hello", "world"]
294
+ item.upcase
295
+ end
296
+ end
297
+ end
298
+ "#;
299
+ analyze(source);
300
+ }
301
+ }
@@ -1,15 +1,19 @@
1
+ mod assignments;
1
2
  mod attributes;
2
3
  mod blocks;
3
4
  mod calls;
4
5
  mod conditionals;
5
6
  mod definitions;
7
+ mod exceptions;
6
8
  mod dispatch;
7
9
  mod install;
8
10
  mod literals;
11
+ mod loops;
9
12
  mod operators;
10
13
  mod parameters;
11
14
  mod parentheses;
12
15
  mod returns;
16
+ mod super_calls;
13
17
  mod variables;
14
18
 
15
19
  pub use install::AstInstaller;
@@ -1,42 +1,52 @@
1
- //! Operators - logical operator type inference (&&, ||)
1
+ //! Operators - logical operator type inference (&&, ||, !)
2
2
 
3
3
  use crate::env::{GlobalEnv, LocalEnv};
4
4
  use crate::graph::{ChangeSet, VertexId};
5
- use ruby_prism::{AndNode, OrNode};
5
+ use crate::types::Type;
6
+ use ruby_prism::{AndNode, Node, OrNode};
6
7
 
7
8
  use super::install::install_node;
8
9
 
9
- /// Process AndNode (a && b): Union(type(a), type(b))
10
- ///
11
- /// Short-circuit semantics: if `a` is falsy, returns `a`; otherwise returns `b`.
12
- /// Static approximation: we cannot determine truthiness at compile time,
13
- /// so we conservatively produce Union(type(a), type(b)).
14
- pub(crate) fn process_and_node(
10
+ /// Merge two branch nodes into a union type vertex.
11
+ fn process_binary_logical_op<'a>(
15
12
  genv: &mut GlobalEnv,
16
13
  lenv: &mut LocalEnv,
17
14
  changes: &mut ChangeSet,
18
15
  source: &str,
19
- and_node: &AndNode,
16
+ left: Node<'a>,
17
+ right: Node<'a>,
20
18
  ) -> Option<VertexId> {
21
19
  let result_vtx = genv.new_vertex();
22
20
 
23
- let left_vtx = install_node(genv, lenv, changes, source, &and_node.left());
24
- if let Some(vtx) = left_vtx {
21
+ if let Some(vtx) = install_node(genv, lenv, changes, source, &left) {
25
22
  genv.add_edge(vtx, result_vtx);
26
23
  }
27
24
 
28
- let right_vtx = install_node(genv, lenv, changes, source, &and_node.right());
29
- if let Some(vtx) = right_vtx {
25
+ if let Some(vtx) = install_node(genv, lenv, changes, source, &right) {
30
26
  genv.add_edge(vtx, result_vtx);
31
27
  }
32
28
 
33
29
  Some(result_vtx)
34
30
  }
35
31
 
32
+ /// Process AndNode (a && b): Union(type(a), type(b))
33
+ ///
34
+ /// Runtime: if `a` is falsy, returns `a`; otherwise returns `b`.
35
+ /// Static: conservatively produce Union(type(a), type(b)).
36
+ pub(crate) fn process_and_node(
37
+ genv: &mut GlobalEnv,
38
+ lenv: &mut LocalEnv,
39
+ changes: &mut ChangeSet,
40
+ source: &str,
41
+ and_node: &AndNode,
42
+ ) -> Option<VertexId> {
43
+ process_binary_logical_op(genv, lenv, changes, source, and_node.left(), and_node.right())
44
+ }
45
+
36
46
  /// Process OrNode (a || b): Union(type(a), type(b))
37
47
  ///
38
- /// Short-circuit semantics: if `a` is truthy, returns `a`; otherwise returns `b`.
39
- /// Static approximation: identical to AndNode — Union of both sides.
48
+ /// Runtime: if `a` is truthy, returns `a`; otherwise returns `b`.
49
+ /// Static: conservatively produce Union(type(a), type(b)).
40
50
  pub(crate) fn process_or_node(
41
51
  genv: &mut GlobalEnv,
42
52
  lenv: &mut LocalEnv,
@@ -44,19 +54,28 @@ pub(crate) fn process_or_node(
44
54
  source: &str,
45
55
  or_node: &OrNode,
46
56
  ) -> Option<VertexId> {
47
- let result_vtx = genv.new_vertex();
48
-
49
- let left_vtx = install_node(genv, lenv, changes, source, &or_node.left());
50
- if let Some(vtx) = left_vtx {
51
- genv.add_edge(vtx, result_vtx);
52
- }
53
-
54
- let right_vtx = install_node(genv, lenv, changes, source, &or_node.right());
55
- if let Some(vtx) = right_vtx {
56
- genv.add_edge(vtx, result_vtx);
57
- }
57
+ process_binary_logical_op(genv, lenv, changes, source, or_node.left(), or_node.right())
58
+ }
58
59
 
59
- Some(result_vtx)
60
+ /// Process not operator (!expr): TrueClass | FalseClass
61
+ ///
62
+ /// In ruby-prism, `!expr` is represented as a CallNode with method name "!".
63
+ /// Static approximation: we cannot determine the receiver's truthiness at
64
+ /// compile time, so conservatively return TrueClass | FalseClass for any `!` call.
65
+ /// In practice, `!nil` and `!false` are always true, but we do not track that here.
66
+ ///
67
+ /// Receiver side effects are already analyzed by the caller (process_needs_child).
68
+ ///
69
+ /// TODO: Ruby allows overriding `BasicObject#!`. Currently we always return
70
+ /// TrueClass | FalseClass, ignoring user-defined `!` methods. If needed, look up
71
+ /// the receiver's RBS definition and use its return type instead.
72
+ pub(crate) fn process_not_operator(genv: &mut GlobalEnv) -> VertexId {
73
+ let result_vtx = genv.new_vertex();
74
+ let true_vtx = genv.new_source(Type::instance("TrueClass"));
75
+ let false_vtx = genv.new_source(Type::instance("FalseClass"));
76
+ genv.add_edge(true_vtx, result_vtx);
77
+ genv.add_edge(false_vtx, result_vtx);
78
+ result_vtx
60
79
  }
61
80
 
62
81
  #[cfg(test)]
@@ -189,4 +208,77 @@ end
189
208
  assert!(type_str.contains("Symbol"), "should contain Symbol: {}", type_str);
190
209
  }
191
210
 
211
+ // ============================================
212
+ // Not operator (!) tests
213
+ // ============================================
214
+
215
+ #[test]
216
+ fn test_not_operator_returns_boolean_union() {
217
+ let source = r#"
218
+ class Foo
219
+ def bar
220
+ !true
221
+ end
222
+ end
223
+ "#;
224
+ let genv = analyze(source);
225
+ let info = genv
226
+ .resolve_method(&Type::instance("Foo"), "bar")
227
+ .expect("bar should be registered");
228
+ let ty = get_type_show(&genv, info.return_vertex.unwrap());
229
+ assert!(ty.contains("TrueClass"), "expected TrueClass in {}", ty);
230
+ assert!(ty.contains("FalseClass"), "expected FalseClass in {}", ty);
231
+ }
232
+
233
+ #[test]
234
+ fn test_not_operator_receiver_side_effects_analyzed() {
235
+ let source = r#"
236
+ class Foo
237
+ def bar
238
+ !(1.upcase)
239
+ end
240
+ end
241
+ "#;
242
+ let genv = analyze(source);
243
+ assert!(
244
+ !genv.type_errors.is_empty(),
245
+ "expected type error for Integer#upcase"
246
+ );
247
+ }
248
+
249
+ #[test]
250
+ fn test_double_not_operator_union() {
251
+ let source = r#"
252
+ class Foo
253
+ def bar
254
+ !!true
255
+ end
256
+ end
257
+ "#;
258
+ let genv = analyze(source);
259
+ let info = genv
260
+ .resolve_method(&Type::instance("Foo"), "bar")
261
+ .expect("bar should be registered");
262
+ let ty = get_type_show(&genv, info.return_vertex.unwrap());
263
+ assert!(ty.contains("TrueClass"), "expected TrueClass in {}", ty);
264
+ assert!(ty.contains("FalseClass"), "expected FalseClass in {}", ty);
265
+ }
266
+
267
+ #[test]
268
+ fn test_not_nil_returns_boolean() {
269
+ let source = r#"
270
+ class Foo
271
+ def bar
272
+ !nil
273
+ end
274
+ end
275
+ "#;
276
+ let genv = analyze(source);
277
+ let info = genv
278
+ .resolve_method(&Type::instance("Foo"), "bar")
279
+ .expect("bar should be registered");
280
+ let ty = get_type_show(&genv, info.return_vertex.unwrap());
281
+ assert!(ty.contains("TrueClass"), "expected TrueClass in {}", ty);
282
+ assert!(ty.contains("FalseClass"), "expected FalseClass in {}", ty);
283
+ }
192
284
  }