method-ray 0.1.1

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 (58) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +23 -0
  3. data/LICENSE +21 -0
  4. data/README.md +39 -0
  5. data/exe/methodray +7 -0
  6. data/ext/Cargo.toml +24 -0
  7. data/ext/extconf.rb +40 -0
  8. data/ext/src/cli.rs +33 -0
  9. data/ext/src/lib.rs +79 -0
  10. data/lib/methodray/cli.rb +28 -0
  11. data/lib/methodray/commands.rb +78 -0
  12. data/lib/methodray/version.rb +5 -0
  13. data/lib/methodray.rb +9 -0
  14. data/rust/Cargo.toml +39 -0
  15. data/rust/src/analyzer/calls.rs +56 -0
  16. data/rust/src/analyzer/definitions.rs +70 -0
  17. data/rust/src/analyzer/dispatch.rs +134 -0
  18. data/rust/src/analyzer/install.rs +226 -0
  19. data/rust/src/analyzer/literals.rs +85 -0
  20. data/rust/src/analyzer/mod.rs +11 -0
  21. data/rust/src/analyzer/tests/integration_test.rs +136 -0
  22. data/rust/src/analyzer/tests/mod.rs +1 -0
  23. data/rust/src/analyzer/variables.rs +76 -0
  24. data/rust/src/cache/mod.rs +3 -0
  25. data/rust/src/cache/rbs_cache.rs +158 -0
  26. data/rust/src/checker.rs +139 -0
  27. data/rust/src/cli/args.rs +40 -0
  28. data/rust/src/cli/commands.rs +139 -0
  29. data/rust/src/cli/mod.rs +6 -0
  30. data/rust/src/diagnostics/diagnostic.rs +125 -0
  31. data/rust/src/diagnostics/formatter.rs +119 -0
  32. data/rust/src/diagnostics/mod.rs +5 -0
  33. data/rust/src/env/box_manager.rs +121 -0
  34. data/rust/src/env/global_env.rs +279 -0
  35. data/rust/src/env/local_env.rs +58 -0
  36. data/rust/src/env/method_registry.rs +63 -0
  37. data/rust/src/env/mod.rs +15 -0
  38. data/rust/src/env/scope.rs +330 -0
  39. data/rust/src/env/type_error.rs +23 -0
  40. data/rust/src/env/vertex_manager.rs +195 -0
  41. data/rust/src/graph/box.rs +157 -0
  42. data/rust/src/graph/change_set.rs +115 -0
  43. data/rust/src/graph/mod.rs +7 -0
  44. data/rust/src/graph/vertex.rs +167 -0
  45. data/rust/src/lib.rs +24 -0
  46. data/rust/src/lsp/diagnostics.rs +133 -0
  47. data/rust/src/lsp/main.rs +8 -0
  48. data/rust/src/lsp/mod.rs +4 -0
  49. data/rust/src/lsp/server.rs +138 -0
  50. data/rust/src/main.rs +46 -0
  51. data/rust/src/parser.rs +96 -0
  52. data/rust/src/rbs/converter.rs +82 -0
  53. data/rust/src/rbs/error.rs +37 -0
  54. data/rust/src/rbs/loader.rs +183 -0
  55. data/rust/src/rbs/mod.rs +15 -0
  56. data/rust/src/source_map.rs +102 -0
  57. data/rust/src/types.rs +75 -0
  58. metadata +119 -0
@@ -0,0 +1,96 @@
1
+ use anyhow::{Context, Result};
2
+ use ruby_prism::{parse, ParseResult};
3
+ use std::fs;
4
+ use std::path::Path;
5
+
6
+ /// Parse Ruby source code and return ruby-prism AST
7
+ ///
8
+ /// Note: Uses Box::leak internally to ensure 'static lifetime
9
+ pub fn parse_ruby_file(file_path: &Path) -> Result<ParseResult<'static>> {
10
+ let source = fs::read_to_string(file_path)
11
+ .with_context(|| format!("Failed to read file: {}", file_path.display()))?;
12
+
13
+ parse_ruby_source(&source, file_path.to_string_lossy().to_string())
14
+ }
15
+
16
+ /// Parse Ruby source code string
17
+ pub fn parse_ruby_source(source: &str, file_name: String) -> Result<ParseResult<'static>> {
18
+ // ruby-prism accepts &[u8]
19
+ // Use Box::leak to ensure 'static lifetime (memory leak is acceptable for analysis tools)
20
+ let source_bytes: &'static [u8] = Box::leak(source.as_bytes().to_vec().into_boxed_slice());
21
+ let parse_result = parse(source_bytes);
22
+
23
+ // Check parse errors
24
+ let error_messages: Vec<String> = parse_result
25
+ .errors()
26
+ .map(|e| {
27
+ format!(
28
+ "Parse error at offset {}: {}",
29
+ e.location().start_offset(),
30
+ e.message()
31
+ )
32
+ })
33
+ .collect();
34
+
35
+ if !error_messages.is_empty() {
36
+ anyhow::bail!(
37
+ "Failed to parse Ruby source in {}:\n{}",
38
+ file_name,
39
+ error_messages.join("\n")
40
+ );
41
+ }
42
+
43
+ Ok(parse_result)
44
+ }
45
+
46
+ #[cfg(test)]
47
+ mod tests {
48
+ use super::*;
49
+
50
+ #[test]
51
+ fn test_parse_simple_ruby() {
52
+ let source = r#"x = 1
53
+ puts x"#;
54
+ let result = parse_ruby_source(source, "test.rb".to_string());
55
+ assert!(result.is_ok());
56
+ }
57
+
58
+ #[test]
59
+ fn test_parse_string_literal() {
60
+ let source = r#""hello".upcase"#;
61
+ let result = parse_ruby_source(source, "test.rb".to_string());
62
+ assert!(result.is_ok());
63
+ }
64
+
65
+ #[test]
66
+ fn test_parse_array_literal() {
67
+ let source = r#"[1, 2, 3].map { |x| x * 2 }"#;
68
+ let result = parse_ruby_source(source, "test.rb".to_string());
69
+ assert!(result.is_ok());
70
+ }
71
+
72
+ #[test]
73
+ fn test_parse_method_definition() {
74
+ let source = r#"def test_method
75
+ x = "hello"
76
+ x.upcase
77
+ end"#;
78
+ let result = parse_ruby_source(source, "test.rb".to_string());
79
+ assert!(result.is_ok());
80
+ }
81
+
82
+ #[test]
83
+ fn test_parse_invalid_ruby() {
84
+ let source = "def\nend end";
85
+ let result = parse_ruby_source(source, "test.rb".to_string());
86
+ assert!(result.is_err());
87
+ }
88
+
89
+ #[test]
90
+ fn test_parse_method_call() {
91
+ let source = r#"user = User.new
92
+ user.save"#;
93
+ let result = parse_ruby_source(source, "test.rb".to_string());
94
+ assert!(result.is_ok());
95
+ }
96
+ }
@@ -0,0 +1,82 @@
1
+ use crate::types::Type;
2
+
3
+ // RBS Type Converter
4
+ pub struct RbsTypeConverter;
5
+
6
+ impl RbsTypeConverter {
7
+ pub fn parse(rbs_type: &str) -> Type {
8
+ // Handle union types
9
+ if rbs_type.contains(" | ") {
10
+ let parts: Vec<&str> = rbs_type.split(" | ").collect();
11
+ let types: Vec<Type> = parts.iter().map(|s| Self::parse_single(s.trim())).collect();
12
+ return Type::Union(types);
13
+ }
14
+
15
+ Self::parse_single(rbs_type)
16
+ }
17
+
18
+ fn parse_single(rbs_type: &str) -> Type {
19
+ let type_name = rbs_type.trim_start_matches("::");
20
+
21
+ match type_name {
22
+ "bool" => Type::Union(vec![
23
+ Type::Instance {
24
+ class_name: "TrueClass".to_string(),
25
+ },
26
+ Type::Instance {
27
+ class_name: "FalseClass".to_string(),
28
+ },
29
+ ]),
30
+ "void" | "nil" => Type::Nil,
31
+ "untyped" | "top" => Type::Bot,
32
+ _ => Type::Instance {
33
+ class_name: type_name.to_string(),
34
+ },
35
+ }
36
+ }
37
+ }
38
+
39
+ #[cfg(test)]
40
+ mod tests {
41
+ use super::*;
42
+
43
+ #[test]
44
+ fn test_parse_simple_types() {
45
+ match RbsTypeConverter::parse("::String") {
46
+ Type::Instance { class_name } => assert_eq!(class_name, "String"),
47
+ _ => panic!("Expected Instance type"),
48
+ }
49
+
50
+ match RbsTypeConverter::parse("Integer") {
51
+ Type::Instance { class_name } => assert_eq!(class_name, "Integer"),
52
+ _ => panic!("Expected Instance type"),
53
+ }
54
+ }
55
+
56
+ #[test]
57
+ fn test_parse_special_types() {
58
+ assert!(matches!(RbsTypeConverter::parse("nil"), Type::Nil));
59
+ assert!(matches!(RbsTypeConverter::parse("void"), Type::Nil));
60
+ assert!(matches!(RbsTypeConverter::parse("untyped"), Type::Bot));
61
+ }
62
+
63
+ #[test]
64
+ fn test_parse_bool() {
65
+ match RbsTypeConverter::parse("bool") {
66
+ Type::Union(types) => {
67
+ assert_eq!(types.len(), 2);
68
+ }
69
+ _ => panic!("Expected Union type for bool"),
70
+ }
71
+ }
72
+
73
+ #[test]
74
+ fn test_parse_union_types() {
75
+ match RbsTypeConverter::parse("String | Integer") {
76
+ Type::Union(types) => {
77
+ assert_eq!(types.len(), 2);
78
+ }
79
+ _ => panic!("Expected Union type"),
80
+ }
81
+ }
82
+ }
@@ -0,0 +1,37 @@
1
+ use std::fmt;
2
+
3
+ #[derive(Debug)]
4
+ pub enum RbsError {
5
+ RbsNotInstalled,
6
+ LoadError(String),
7
+ ParseError(String),
8
+ MagnusError(magnus::Error),
9
+ }
10
+
11
+ impl fmt::Display for RbsError {
12
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
13
+ match self {
14
+ RbsError::RbsNotInstalled => {
15
+ write!(f, "RBS gem not found. Please install: gem install rbs")
16
+ }
17
+ RbsError::LoadError(msg) => write!(f, "Failed to load RBS environment: {}", msg),
18
+ RbsError::ParseError(msg) => write!(f, "Failed to parse RBS type: {}", msg),
19
+ RbsError::MagnusError(e) => write!(f, "Magnus error: {}", e),
20
+ }
21
+ }
22
+ }
23
+
24
+ impl std::error::Error for RbsError {}
25
+
26
+ impl From<magnus::Error> for RbsError {
27
+ fn from(err: magnus::Error) -> Self {
28
+ RbsError::MagnusError(err)
29
+ }
30
+ }
31
+
32
+ impl From<RbsError> for magnus::Error {
33
+ fn from(err: RbsError) -> Self {
34
+ let ruby = unsafe { magnus::Ruby::get_unchecked() };
35
+ magnus::Error::new(ruby.exception_runtime_error(), err.to_string())
36
+ }
37
+ }
@@ -0,0 +1,183 @@
1
+ use crate::env::GlobalEnv;
2
+ use crate::rbs::converter::RbsTypeConverter;
3
+ use crate::rbs::error::RbsError;
4
+ use crate::types::Type;
5
+ use magnus::{Error, RArray, RHash, Ruby, TryConvert, Value};
6
+
7
+ /// Method information loaded from RBS
8
+ #[derive(Debug, Clone)]
9
+ pub struct RbsMethodInfo {
10
+ pub receiver_class: String,
11
+ pub method_name: String,
12
+ pub return_type: Type,
13
+ }
14
+
15
+ /// Loader that calls RBS API via magnus to load method information
16
+ pub struct RbsLoader<'a> {
17
+ ruby: &'a Ruby,
18
+ }
19
+
20
+ impl<'a> RbsLoader<'a> {
21
+ /// Create a new RbsLoader
22
+ /// Assumes RBS gem is already loaded on the Ruby side
23
+ pub fn new(ruby: &'a Ruby) -> Result<Self, RbsError> {
24
+ // Check if RBS module is defined
25
+ let rbs_defined: bool = ruby
26
+ .eval("defined?(RBS) == 'constant'")
27
+ .map_err(|e| RbsError::LoadError(format!("Failed to check RBS: {}", e)))?;
28
+
29
+ if !rbs_defined {
30
+ return Err(RbsError::RbsNotInstalled);
31
+ }
32
+
33
+ Ok(Self { ruby })
34
+ }
35
+
36
+ /// Load all method definitions from RBS
37
+ pub fn load_methods(&self) -> Result<Vec<RbsMethodInfo>, RbsError> {
38
+ // Load method_loader.rb
39
+ let rb_path = concat!(env!("CARGO_MANIFEST_DIR"), "/src/rbs/method_loader.rb");
40
+ let load_code = format!("require '{}'", rb_path);
41
+ let _: Value = self
42
+ .ruby
43
+ .eval(&load_code)
44
+ .map_err(|e| RbsError::LoadError(format!("Failed to load method_loader.rb: {}", e)))?;
45
+
46
+ // Instantiate Rbs::MethodLoader class and call method
47
+ let results: Value = self
48
+ .ruby
49
+ .eval("Rbs::MethodLoader.new.load_methods")
50
+ .map_err(|e| {
51
+ RbsError::LoadError(format!(
52
+ "Failed to call Rbs::MethodLoader#load_methods: {}",
53
+ e
54
+ ))
55
+ })?;
56
+
57
+ self.parse_results(results)
58
+ }
59
+
60
+ /// Convert Ruby array results to Vec of RbsMethodInfo structs
61
+ fn parse_results(&self, results: Value) -> Result<Vec<RbsMethodInfo>, RbsError> {
62
+ let mut method_infos = Vec::new();
63
+
64
+ // Convert to Ruby array
65
+ let results_array = RArray::try_convert(results)
66
+ .map_err(|e| RbsError::ParseError(format!("Failed to convert to array: {}", e)))?;
67
+
68
+ // Process each element
69
+ for entry in results_array.into_iter() {
70
+ let entry: Value = entry;
71
+
72
+ // Convert to hash
73
+ let hash = RHash::try_convert(entry).map_err(|e| {
74
+ RbsError::ParseError(format!("Failed to convert entry to hash: {}", e))
75
+ })?;
76
+
77
+ // Get each field
78
+ let receiver_class_value = hash
79
+ .get(self.ruby.to_symbol("receiver_class"))
80
+ .ok_or_else(|| RbsError::ParseError("Missing receiver_class".to_string()))?;
81
+ let receiver_class: String =
82
+ String::try_convert(receiver_class_value).map_err(|e| {
83
+ RbsError::ParseError(format!("Failed to convert receiver_class: {}", e))
84
+ })?;
85
+
86
+ let method_name_value = hash
87
+ .get(self.ruby.to_symbol("method_name"))
88
+ .ok_or_else(|| RbsError::ParseError("Missing method_name".to_string()))?;
89
+ let method_name: String = String::try_convert(method_name_value).map_err(|e| {
90
+ RbsError::ParseError(format!("Failed to convert method_name: {}", e))
91
+ })?;
92
+
93
+ let return_type_value = hash
94
+ .get(self.ruby.to_symbol("return_type"))
95
+ .ok_or_else(|| RbsError::ParseError("Missing return_type".to_string()))?;
96
+ let return_type_str: String = String::try_convert(return_type_value).map_err(|e| {
97
+ RbsError::ParseError(format!("Failed to convert return_type: {}", e))
98
+ })?;
99
+
100
+ // Convert RBS type string to internal Type enum
101
+ let return_type = RbsTypeConverter::parse(&return_type_str);
102
+
103
+ method_infos.push(RbsMethodInfo {
104
+ receiver_class,
105
+ method_name,
106
+ return_type,
107
+ });
108
+ }
109
+
110
+ Ok(method_infos)
111
+ }
112
+ }
113
+
114
+ /// Helper function to register RBS methods to GlobalEnv
115
+ /// Uses cache to avoid slow Ruby FFI calls
116
+ pub fn register_rbs_methods(genv: &mut GlobalEnv, ruby: &Ruby) -> Result<usize, Error> {
117
+ use crate::cache::RbsCache;
118
+
119
+ let methodray_version = env!("CARGO_PKG_VERSION");
120
+
121
+ // Try to get RBS version
122
+ let rbs_version_value: Value = ruby
123
+ .eval("RBS::VERSION")
124
+ .unwrap_or_else(|_| ruby.eval("'unknown'").unwrap());
125
+ let rbs_version: String =
126
+ String::try_convert(rbs_version_value).unwrap_or_else(|_| "unknown".to_string());
127
+
128
+ // Try to load from cache
129
+ let methods = if let Ok(cache) = RbsCache::load() {
130
+ if cache.is_valid(methodray_version, &rbs_version) {
131
+ eprintln!("Loaded {} methods from cache", cache.methods.len());
132
+ cache.to_method_infos()
133
+ } else {
134
+ eprintln!("Cache invalid, reloading from RBS...");
135
+ let methods = load_and_cache_rbs_methods(ruby, methodray_version, &rbs_version)?;
136
+ methods
137
+ }
138
+ } else {
139
+ eprintln!("No cache found, loading from RBS...");
140
+ load_and_cache_rbs_methods(ruby, methodray_version, &rbs_version)?
141
+ };
142
+
143
+ let count = methods.len();
144
+ for method_info in methods {
145
+ let receiver_type = Type::Instance {
146
+ class_name: method_info.receiver_class,
147
+ };
148
+ genv.register_builtin_method(
149
+ receiver_type,
150
+ &method_info.method_name,
151
+ method_info.return_type,
152
+ );
153
+ }
154
+
155
+ Ok(count)
156
+ }
157
+
158
+ /// Load RBS methods and save to cache
159
+ fn load_and_cache_rbs_methods(
160
+ ruby: &Ruby,
161
+ version: &str,
162
+ rbs_version: &str,
163
+ ) -> Result<Vec<RbsMethodInfo>, Error> {
164
+ use crate::cache::RbsCache;
165
+
166
+ let loader = RbsLoader::new(ruby)?;
167
+ let methods = loader.load_methods()?;
168
+
169
+ // Save to cache
170
+ let cache = RbsCache::from_method_infos(
171
+ methods.clone(),
172
+ version.to_string(),
173
+ rbs_version.to_string(),
174
+ );
175
+
176
+ if let Err(e) = cache.save() {
177
+ eprintln!("Warning: Failed to save RBS cache: {}", e);
178
+ } else {
179
+ eprintln!("Saved {} methods to cache", methods.len());
180
+ }
181
+
182
+ Ok(methods)
183
+ }
@@ -0,0 +1,15 @@
1
+ //! RBS type loading and conversion
2
+
3
+ #[cfg(feature = "ruby-ffi")]
4
+ pub mod converter;
5
+ #[cfg(feature = "ruby-ffi")]
6
+ pub mod error;
7
+ #[cfg(feature = "ruby-ffi")]
8
+ pub mod loader;
9
+
10
+ #[cfg(feature = "ruby-ffi")]
11
+ pub use converter::RbsTypeConverter;
12
+ #[cfg(feature = "ruby-ffi")]
13
+ pub use error::RbsError;
14
+ #[cfg(feature = "ruby-ffi")]
15
+ pub use loader::{register_rbs_methods, RbsLoader, RbsMethodInfo};
@@ -0,0 +1,102 @@
1
+ /// Convert byte offset to (line, column) - 1-indexed
2
+ fn offset_to_line_column(source: &str, offset: usize) -> (usize, usize) {
3
+ let mut line = 1;
4
+ let mut column = 1;
5
+ let mut current_offset = 0;
6
+
7
+ for ch in source.chars() {
8
+ if current_offset >= offset {
9
+ break;
10
+ }
11
+
12
+ if ch == '\n' {
13
+ line += 1;
14
+ column = 1;
15
+ } else {
16
+ column += 1;
17
+ }
18
+
19
+ current_offset += ch.len_utf8();
20
+ }
21
+
22
+ (line, column)
23
+ }
24
+
25
+ /// Source code location information
26
+ #[derive(Debug, Clone, PartialEq, Eq, Hash)]
27
+ pub struct SourceLocation {
28
+ pub line: usize,
29
+ pub column: usize,
30
+ pub length: usize,
31
+ }
32
+
33
+ #[allow(dead_code)]
34
+ impl SourceLocation {
35
+ pub fn new(line: usize, column: usize, length: usize) -> Self {
36
+ Self {
37
+ line,
38
+ column,
39
+ length,
40
+ }
41
+ }
42
+
43
+ /// Create from ruby-prism Location and source code
44
+ /// Calculates line/column from byte offset
45
+ pub fn from_prism_location_with_source(location: &ruby_prism::Location, source: &str) -> Self {
46
+ let start_offset = location.start_offset();
47
+ let length = location.end_offset() - start_offset;
48
+
49
+ // Calculate line and column from byte offset
50
+ let (line, column) = offset_to_line_column(source, start_offset);
51
+
52
+ Self {
53
+ line,
54
+ column,
55
+ length,
56
+ }
57
+ }
58
+
59
+ /// Create from ruby-prism Location (without source - uses approximation)
60
+ pub fn from_prism_location(location: &ruby_prism::Location) -> Self {
61
+ let start_offset = location.start_offset();
62
+ let length = location.end_offset() - start_offset;
63
+
64
+ // Without source, we can't calculate exact line/column
65
+ // Use offset as column for now
66
+ Self {
67
+ line: 1, // Placeholder
68
+ column: start_offset + 1,
69
+ length,
70
+ }
71
+ }
72
+ }
73
+
74
+ #[cfg(test)]
75
+ mod tests {
76
+ use super::*;
77
+
78
+ #[test]
79
+ fn test_source_location_creation() {
80
+ let loc = SourceLocation::new(10, 5, 6);
81
+ assert_eq!(loc.line, 10);
82
+ assert_eq!(loc.column, 5);
83
+ assert_eq!(loc.length, 6);
84
+ }
85
+
86
+ #[test]
87
+ fn test_offset_to_line_column() {
88
+ let source = "x = 1\ny = x.upcase";
89
+ // "x = 1\n" is 6 bytes (0-5)
90
+ // "y = x.upcase" starts at offset 6
91
+ // "y = x." is 6 bytes, so ".upcase" starts at offset 12
92
+
93
+ // Test offset 0 (start of line 1)
94
+ assert_eq!(offset_to_line_column(source, 0), (1, 1));
95
+
96
+ // Test offset 6 (start of line 2, after newline)
97
+ assert_eq!(offset_to_line_column(source, 6), (2, 1));
98
+
99
+ // Test offset 10 (the 'x' in 'x.upcase')
100
+ assert_eq!(offset_to_line_column(source, 10), (2, 5));
101
+ }
102
+ }
data/rust/src/types.rs ADDED
@@ -0,0 +1,75 @@
1
+ /// Type system for graph-based type inference
2
+ #[derive(Clone, Debug, PartialEq, Eq, Hash)]
3
+ #[allow(dead_code)]
4
+ pub enum Type {
5
+ /// Instance type: String, Integer, etc.
6
+ Instance { class_name: String },
7
+ /// Singleton type: for class methods
8
+ Singleton { class_name: String },
9
+ /// nil type
10
+ Nil,
11
+ /// Union type: sum of multiple types
12
+ Union(Vec<Type>),
13
+ /// Bottom type: no type information
14
+ Bot,
15
+ }
16
+
17
+ impl Type {
18
+ /// Convert type to string representation
19
+ pub fn show(&self) -> String {
20
+ match self {
21
+ Type::Instance { class_name } => class_name.clone(),
22
+ Type::Singleton { class_name } => format!("singleton({})", class_name),
23
+ Type::Nil => "nil".to_string(),
24
+ Type::Union(types) => {
25
+ let names: Vec<_> = types.iter().map(|t| t.show()).collect();
26
+ names.join(" | ")
27
+ }
28
+ Type::Bot => "untyped".to_string(),
29
+ }
30
+ }
31
+
32
+ /// Convenience constructors
33
+ pub fn string() -> Self {
34
+ Type::Instance {
35
+ class_name: "String".to_string(),
36
+ }
37
+ }
38
+
39
+ pub fn integer() -> Self {
40
+ Type::Instance {
41
+ class_name: "Integer".to_string(),
42
+ }
43
+ }
44
+
45
+ pub fn array() -> Self {
46
+ Type::Instance {
47
+ class_name: "Array".to_string(),
48
+ }
49
+ }
50
+
51
+ pub fn hash() -> Self {
52
+ Type::Instance {
53
+ class_name: "Hash".to_string(),
54
+ }
55
+ }
56
+ }
57
+
58
+ #[cfg(test)]
59
+ mod tests {
60
+ use super::*;
61
+
62
+ #[test]
63
+ fn test_type_show() {
64
+ assert_eq!(Type::string().show(), "String");
65
+ assert_eq!(Type::integer().show(), "Integer");
66
+ assert_eq!(Type::Nil.show(), "nil");
67
+ assert_eq!(Type::Bot.show(), "untyped");
68
+ }
69
+
70
+ #[test]
71
+ fn test_type_union() {
72
+ let union = Type::Union(vec![Type::string(), Type::integer()]);
73
+ assert_eq!(union.show(), "String | Integer");
74
+ }
75
+ }