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,158 @@
1
+ use anyhow::{Context, Result};
2
+ use serde::{Deserialize, Serialize};
3
+ use std::fs;
4
+ use std::path::PathBuf;
5
+ use std::time::SystemTime;
6
+
7
+ #[cfg(feature = "ruby-ffi")]
8
+ use crate::rbs::loader::RbsMethodInfo;
9
+
10
+ /// Binary cache for RBS method definitions
11
+ #[derive(Serialize, Deserialize, Debug)]
12
+ pub struct RbsCache {
13
+ /// MethodRay version
14
+ pub version: String,
15
+ /// RBS gem version
16
+ pub rbs_version: String,
17
+ /// Cached method information
18
+ pub methods: Vec<SerializableMethodInfo>,
19
+ /// Cache creation timestamp
20
+ pub timestamp: SystemTime,
21
+ }
22
+
23
+ /// Serializable version of RbsMethodInfo
24
+ #[derive(Serialize, Deserialize, Debug, Clone)]
25
+ pub struct SerializableMethodInfo {
26
+ pub receiver_class: String,
27
+ pub method_name: String,
28
+ pub return_type_str: String, // Simplified: store as string
29
+ }
30
+
31
+ impl SerializableMethodInfo {
32
+ /// Parse return type string into Type (simple parser for cached data)
33
+ pub fn return_type(&self) -> crate::types::Type {
34
+ crate::types::Type::Instance {
35
+ class_name: self.return_type_str.clone(),
36
+ }
37
+ }
38
+ }
39
+
40
+ #[allow(dead_code)]
41
+ impl RbsCache {
42
+ /// Get cache file path
43
+ pub fn cache_path() -> Result<PathBuf> {
44
+ let cache_dir = dirs::cache_dir()
45
+ .context("Failed to get cache directory")?
46
+ .join("methodray");
47
+
48
+ fs::create_dir_all(&cache_dir).context("Failed to create cache directory")?;
49
+
50
+ Ok(cache_dir.join("rbs_cache.bin"))
51
+ }
52
+
53
+ /// Load cache from disk
54
+ pub fn load() -> Result<Self> {
55
+ let path = Self::cache_path()?;
56
+ let bytes = fs::read(&path)
57
+ .with_context(|| format!("Failed to read cache from {}", path.display()))?;
58
+
59
+ bincode::deserialize(&bytes).context("Failed to deserialize cache")
60
+ }
61
+
62
+ /// Save cache to disk
63
+ pub fn save(&self) -> Result<()> {
64
+ let path = Self::cache_path()?;
65
+ let bytes = bincode::serialize(self).context("Failed to serialize cache")?;
66
+
67
+ fs::write(&path, bytes)
68
+ .with_context(|| format!("Failed to write cache to {}", path.display()))?;
69
+
70
+ Ok(())
71
+ }
72
+
73
+ /// Check if cache is valid
74
+ pub fn is_valid(&self, current_version: &str, current_rbs_version: &str) -> bool {
75
+ self.version == current_version && self.rbs_version == current_rbs_version
76
+ }
77
+
78
+ /// Get methods for registration (works without ruby-ffi feature)
79
+ pub fn methods(&self) -> &[SerializableMethodInfo] {
80
+ &self.methods
81
+ }
82
+
83
+ /// Convert to RbsMethodInfo (requires ruby-ffi for full type parsing)
84
+ #[cfg(feature = "ruby-ffi")]
85
+ pub fn to_method_infos(&self) -> Vec<RbsMethodInfo> {
86
+ self.methods
87
+ .iter()
88
+ .map(|m| RbsMethodInfo {
89
+ receiver_class: m.receiver_class.clone(),
90
+ method_name: m.method_name.clone(),
91
+ return_type: crate::rbs::converter::RbsTypeConverter::parse(&m.return_type_str),
92
+ })
93
+ .collect()
94
+ }
95
+
96
+ /// Create from RbsMethodInfo
97
+ #[cfg(feature = "ruby-ffi")]
98
+ pub fn from_method_infos(
99
+ methods: Vec<RbsMethodInfo>,
100
+ version: String,
101
+ rbs_version: String,
102
+ ) -> Self {
103
+ let serializable_methods = methods
104
+ .into_iter()
105
+ .map(|m| SerializableMethodInfo {
106
+ receiver_class: m.receiver_class,
107
+ method_name: m.method_name,
108
+ return_type_str: m.return_type.show(),
109
+ })
110
+ .collect();
111
+
112
+ Self {
113
+ version,
114
+ rbs_version,
115
+ methods: serializable_methods,
116
+ timestamp: SystemTime::now(),
117
+ }
118
+ }
119
+ }
120
+
121
+ #[cfg(test)]
122
+ mod tests {
123
+ use super::*;
124
+
125
+ #[test]
126
+ fn test_cache_serialization() {
127
+ let cache = RbsCache {
128
+ version: "0.1.0".to_string(),
129
+ rbs_version: "3.7.0".to_string(),
130
+ methods: vec![SerializableMethodInfo {
131
+ receiver_class: "String".to_string(),
132
+ method_name: "upcase".to_string(),
133
+ return_type_str: "String".to_string(),
134
+ }],
135
+ timestamp: SystemTime::now(),
136
+ };
137
+
138
+ let bytes = bincode::serialize(&cache).unwrap();
139
+ let deserialized: RbsCache = bincode::deserialize(&bytes).unwrap();
140
+
141
+ assert_eq!(deserialized.version, "0.1.0");
142
+ assert_eq!(deserialized.methods.len(), 1);
143
+ }
144
+
145
+ #[test]
146
+ fn test_cache_validation() {
147
+ let cache = RbsCache {
148
+ version: "0.1.0".to_string(),
149
+ rbs_version: "3.7.0".to_string(),
150
+ methods: vec![],
151
+ timestamp: SystemTime::now(),
152
+ };
153
+
154
+ assert!(cache.is_valid("0.1.0", "3.7.0"));
155
+ assert!(!cache.is_valid("0.2.0", "3.7.0"));
156
+ assert!(!cache.is_valid("0.1.0", "3.8.0"));
157
+ }
158
+ }
@@ -0,0 +1,139 @@
1
+ use crate::analyzer::AstInstaller;
2
+ use crate::diagnostics::Diagnostic;
3
+ use crate::env::{GlobalEnv, LocalEnv};
4
+ use crate::parser;
5
+ use anyhow::{Context, Result};
6
+ use std::path::Path;
7
+
8
+ /// File type checker
9
+ pub struct FileChecker {
10
+ // No state - creates fresh GlobalEnv for each check
11
+ }
12
+
13
+ impl FileChecker {
14
+ /// Create new FileChecker
15
+ /// Note: This is for standalone CLI usage (no Ruby runtime)
16
+ pub fn new() -> Result<Self> {
17
+ // Just verify cache exists
18
+ use crate::cache::RbsCache;
19
+ RbsCache::load().context(
20
+ "Failed to load RBS cache. Please run from Ruby first to generate cache:\n\
21
+ ruby -rmethodray -e 'MethodRay::Analyzer.new(\".\").infer_types(\"x=1\")'",
22
+ )?;
23
+
24
+ Ok(Self {})
25
+ }
26
+
27
+ /// Check a single Ruby file
28
+ pub fn check_file(&self, file_path: &Path) -> Result<Vec<Diagnostic>> {
29
+ // Read source code
30
+ let source = std::fs::read_to_string(file_path)
31
+ .with_context(|| format!("Failed to read {}", file_path.display()))?;
32
+
33
+ // Parse file
34
+ let parse_result = parser::parse_ruby_file(file_path)
35
+ .with_context(|| format!("Failed to parse {}", file_path.display()))?;
36
+
37
+ // Create fresh GlobalEnv for this analysis
38
+ let mut genv = GlobalEnv::new();
39
+ load_rbs_from_cache(&mut genv)?;
40
+
41
+ let mut lenv = LocalEnv::new();
42
+ let mut installer = AstInstaller::new(&mut genv, &mut lenv, &source);
43
+
44
+ // Process AST
45
+ let root = parse_result.node();
46
+ if let Some(program_node) = root.as_program_node() {
47
+ let statements = program_node.statements();
48
+ for stmt in &statements.body() {
49
+ installer.install_node(&stmt);
50
+ }
51
+ }
52
+
53
+ installer.finish();
54
+
55
+ // Collect diagnostics
56
+ let diagnostics = collect_diagnostics(&genv, file_path);
57
+
58
+ Ok(diagnostics)
59
+ }
60
+ }
61
+
62
+ /// Load RBS methods from cache (CLI mode without Ruby runtime)
63
+ fn load_rbs_from_cache(genv: &mut GlobalEnv) -> Result<()> {
64
+ use crate::cache::RbsCache;
65
+ use crate::types::Type;
66
+
67
+ let cache = RbsCache::load().context(
68
+ "Failed to load RBS cache. Please run from Ruby first to generate cache:\n\
69
+ ruby -rmethodray -e 'MethodRay::Analyzer.new(\".\").infer_types(\"x=1\")'",
70
+ )?;
71
+
72
+ let methods = cache.methods();
73
+ eprintln!("Loaded {} methods from cache", methods.len());
74
+
75
+ for method_info in methods {
76
+ let receiver_type = Type::Instance {
77
+ class_name: method_info.receiver_class.clone(),
78
+ };
79
+ genv.register_builtin_method(
80
+ receiver_type,
81
+ &method_info.method_name,
82
+ method_info.return_type(),
83
+ );
84
+ }
85
+
86
+ Ok(())
87
+ }
88
+
89
+ /// Collect type error diagnostics from GlobalEnv
90
+ fn collect_diagnostics(genv: &GlobalEnv, file_path: &Path) -> Vec<Diagnostic> {
91
+ use crate::diagnostics::{Diagnostic, Location};
92
+ use std::path::PathBuf;
93
+
94
+ let mut diagnostics = Vec::new();
95
+
96
+ // Convert TypeErrors to Diagnostics
97
+ for type_error in &genv.type_errors {
98
+ // Use actual location from TypeError if available
99
+ let location = if let Some(source_loc) = &type_error.location {
100
+ Location {
101
+ file: PathBuf::from(file_path),
102
+ line: source_loc.line,
103
+ column: source_loc.column,
104
+ length: Some(source_loc.length),
105
+ }
106
+ } else {
107
+ // Fallback to placeholder
108
+ Location {
109
+ file: PathBuf::from(file_path),
110
+ line: 1,
111
+ column: 1,
112
+ length: None,
113
+ }
114
+ };
115
+
116
+ let diagnostic = Diagnostic::undefined_method(
117
+ location,
118
+ &type_error.receiver_type.show(),
119
+ &type_error.method_name,
120
+ );
121
+
122
+ diagnostics.push(diagnostic);
123
+ }
124
+
125
+ diagnostics
126
+ }
127
+
128
+ #[cfg(test)]
129
+ mod tests {
130
+ use super::*;
131
+
132
+ #[test]
133
+ fn test_file_checker_creation() {
134
+ // This test will fail if RBS cache doesn't exist
135
+ // That's expected - cache should be generated from Ruby side first
136
+ let result = FileChecker::new();
137
+ assert!(result.is_ok() || result.is_err()); // Just check it doesn't panic
138
+ }
139
+ }
@@ -0,0 +1,40 @@
1
+ //! CLI argument definitions
2
+
3
+ use clap::{Parser, Subcommand};
4
+ use std::path::PathBuf;
5
+
6
+ /// MethodRay - Fast Ruby type checker
7
+ #[derive(Parser)]
8
+ #[command(name = "methodray")]
9
+ #[command(about = "Fast Ruby type checker with method chain validation", long_about = None)]
10
+ pub struct Cli {
11
+ #[command(subcommand)]
12
+ pub command: Commands,
13
+ }
14
+
15
+ #[derive(Subcommand)]
16
+ pub enum Commands {
17
+ /// Check Ruby file(s) for type errors
18
+ Check {
19
+ /// Ruby file to check (if not specified, checks all files in project)
20
+ #[arg(value_name = "FILE")]
21
+ file: Option<PathBuf>,
22
+
23
+ /// Show detailed output
24
+ #[arg(short, long)]
25
+ verbose: bool,
26
+ },
27
+
28
+ /// Watch a Ruby file and re-check on changes
29
+ Watch {
30
+ /// Ruby file to watch
31
+ #[arg(value_name = "FILE")]
32
+ file: PathBuf,
33
+ },
34
+
35
+ /// Show version information
36
+ Version,
37
+
38
+ /// Clear RBS cache
39
+ ClearCache,
40
+ }
@@ -0,0 +1,139 @@
1
+ //! CLI command implementations
2
+
3
+ use anyhow::Result;
4
+ use std::path::PathBuf;
5
+
6
+ use crate::cache::RbsCache;
7
+ use crate::checker::FileChecker;
8
+ use crate::diagnostics;
9
+
10
+ /// Check a single Ruby file for type errors
11
+ /// Returns Ok(true) if no errors, Ok(false) if errors found
12
+ pub fn check_single_file(file_path: &PathBuf, verbose: bool) -> Result<bool> {
13
+ let checker = FileChecker::new()?;
14
+ let diagnostics = checker.check_file(file_path)?;
15
+
16
+ if diagnostics.is_empty() {
17
+ if verbose {
18
+ println!("{}: No errors found", file_path.display());
19
+ }
20
+ Ok(true)
21
+ } else {
22
+ let output = diagnostics::format_diagnostics_with_file(&diagnostics, file_path);
23
+ println!("{}", output);
24
+
25
+ let has_errors = diagnostics
26
+ .iter()
27
+ .any(|d| d.level == diagnostics::DiagnosticLevel::Error);
28
+
29
+ Ok(!has_errors)
30
+ }
31
+ }
32
+
33
+ /// Check all Ruby files in the project
34
+ pub fn check_project(_verbose: bool) -> Result<()> {
35
+ println!("Project-wide checking not yet implemented");
36
+ println!("Use: methodray check <file> to check a single file");
37
+ Ok(())
38
+ }
39
+
40
+ /// Watch a file for changes and re-check on modifications
41
+ pub fn watch_file(file_path: &PathBuf) -> Result<()> {
42
+ use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher};
43
+ use std::sync::mpsc::channel;
44
+ use std::time::Duration;
45
+
46
+ if !file_path.exists() {
47
+ anyhow::bail!("File not found: {}", file_path.display());
48
+ }
49
+
50
+ println!(
51
+ "Watching {} for changes (Press Ctrl+C to stop)",
52
+ file_path.display()
53
+ );
54
+ println!();
55
+
56
+ // Initial check
57
+ println!("Initial check:");
58
+ let mut had_errors = match check_single_file(file_path, true) {
59
+ Ok(success) => !success,
60
+ Err(e) => {
61
+ eprintln!("Error during initial check: {}", e);
62
+ true
63
+ }
64
+ };
65
+ println!();
66
+
67
+ // Setup file watcher
68
+ let (tx, rx) = channel();
69
+
70
+ let mut watcher = RecommendedWatcher::new(
71
+ move |res| {
72
+ if let Ok(event) = res {
73
+ let _ = tx.send(event);
74
+ }
75
+ },
76
+ Config::default().with_poll_interval(Duration::from_millis(500)),
77
+ )?;
78
+
79
+ watcher.watch(file_path.as_ref(), RecursiveMode::NonRecursive)?;
80
+
81
+ // Event loop
82
+ loop {
83
+ match rx.recv() {
84
+ Ok(event) => {
85
+ if let notify::EventKind::Modify(_) = event.kind {
86
+ println!("\n--- File changed, re-checking... ---\n");
87
+
88
+ std::thread::sleep(Duration::from_millis(100));
89
+
90
+ match check_single_file(file_path, true) {
91
+ Ok(success) => {
92
+ if success && had_errors {
93
+ println!("✓ All errors fixed!");
94
+ had_errors = false;
95
+ } else if !success && !had_errors {
96
+ had_errors = true;
97
+ }
98
+ }
99
+ Err(e) => {
100
+ eprintln!("Error during check: {}", e);
101
+ had_errors = true;
102
+ }
103
+ }
104
+ println!();
105
+ }
106
+ }
107
+ Err(e) => {
108
+ eprintln!("Watch error: {}", e);
109
+ break;
110
+ }
111
+ }
112
+ }
113
+
114
+ Ok(())
115
+ }
116
+
117
+ /// Clear the RBS cache
118
+ pub fn clear_cache() -> Result<()> {
119
+ match RbsCache::cache_path() {
120
+ Ok(path) => {
121
+ if path.exists() {
122
+ std::fs::remove_file(&path)?;
123
+ println!("Cache cleared: {}", path.display());
124
+ } else {
125
+ println!("No cache file found");
126
+ }
127
+ }
128
+ Err(e) => {
129
+ eprintln!("Failed to get cache path: {}", e);
130
+ }
131
+ }
132
+
133
+ Ok(())
134
+ }
135
+
136
+ /// Print version information
137
+ pub fn print_version() {
138
+ println!("MethodRay {}", env!("CARGO_PKG_VERSION"));
139
+ }
@@ -0,0 +1,6 @@
1
+ //! CLI module - Command-line interface for MethodRay
2
+
3
+ pub mod args;
4
+ pub mod commands;
5
+
6
+ pub use args::{Cli, Commands};
@@ -0,0 +1,125 @@
1
+ use std::path::PathBuf;
2
+
3
+ /// Diagnostic severity level (LSP compatible)
4
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
5
+ #[allow(dead_code)]
6
+ pub enum DiagnosticLevel {
7
+ Error,
8
+ Warning,
9
+ }
10
+
11
+ impl DiagnosticLevel {
12
+ pub fn as_str(&self) -> &str {
13
+ match self {
14
+ DiagnosticLevel::Error => "error",
15
+ DiagnosticLevel::Warning => "warning",
16
+ }
17
+ }
18
+ }
19
+
20
+ /// Source code location
21
+ #[derive(Debug, Clone, PartialEq, Eq)]
22
+ pub struct Location {
23
+ pub file: PathBuf,
24
+ pub line: usize,
25
+ pub column: usize,
26
+ pub length: Option<usize>, // Character length of the error span
27
+ }
28
+
29
+ /// Type checking diagnostic
30
+ #[derive(Debug, Clone)]
31
+ #[allow(dead_code)]
32
+ pub struct Diagnostic {
33
+ pub location: Location,
34
+ pub level: DiagnosticLevel,
35
+ pub message: String,
36
+ pub code: Option<String>, // e.g., "E001"
37
+ }
38
+
39
+ #[allow(dead_code)]
40
+ impl Diagnostic {
41
+ /// Create an error diagnostic
42
+ pub fn error(location: Location, message: String) -> Self {
43
+ Self {
44
+ location,
45
+ level: DiagnosticLevel::Error,
46
+ message,
47
+ code: None,
48
+ }
49
+ }
50
+
51
+ /// Create a warning diagnostic (internal use only)
52
+ fn warning(location: Location, message: String) -> Self {
53
+ Self {
54
+ location,
55
+ level: DiagnosticLevel::Warning,
56
+ message,
57
+ code: None,
58
+ }
59
+ }
60
+
61
+ /// Create undefined method error
62
+ pub fn undefined_method(location: Location, receiver_type: &str, method_name: &str) -> Self {
63
+ Self::error(
64
+ location,
65
+ format!("undefined method `{}` for {}", method_name, receiver_type),
66
+ )
67
+ }
68
+
69
+ /// Create Union type partial error (warning)
70
+ pub fn union_partial_error(
71
+ location: Location,
72
+ valid_types: Vec<String>,
73
+ invalid_types: Vec<String>,
74
+ method_name: &str,
75
+ ) -> Self {
76
+ let message = format!(
77
+ "method `{}` is defined for {} but not for {}",
78
+ method_name,
79
+ valid_types.join(", "),
80
+ invalid_types.join(", ")
81
+ );
82
+ Self::warning(location, message)
83
+ }
84
+ }
85
+
86
+ #[cfg(test)]
87
+ mod tests {
88
+ use super::*;
89
+
90
+ #[test]
91
+ fn test_diagnostic_creation() {
92
+ let loc = Location {
93
+ file: PathBuf::from("test.rb"),
94
+ line: 10,
95
+ column: 5,
96
+ length: None,
97
+ };
98
+
99
+ let diag = Diagnostic::undefined_method(loc.clone(), "Integer", "upcase");
100
+ assert_eq!(diag.level, DiagnosticLevel::Error);
101
+ assert_eq!(diag.message, "undefined method `upcase` for Integer");
102
+ }
103
+
104
+ #[test]
105
+ fn test_union_partial_error() {
106
+ let loc = Location {
107
+ file: PathBuf::from("test.rb"),
108
+ line: 15,
109
+ column: 3,
110
+ length: None,
111
+ };
112
+
113
+ let diag = Diagnostic::union_partial_error(
114
+ loc,
115
+ vec!["String".to_string()],
116
+ vec!["Integer".to_string()],
117
+ "upcase",
118
+ );
119
+
120
+ assert_eq!(diag.level, DiagnosticLevel::Warning);
121
+ assert!(diag
122
+ .message
123
+ .contains("method `upcase` is defined for String but not for Integer"));
124
+ }
125
+ }