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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +23 -0
- data/LICENSE +21 -0
- data/README.md +39 -0
- data/exe/methodray +7 -0
- data/ext/Cargo.toml +24 -0
- data/ext/extconf.rb +40 -0
- data/ext/src/cli.rs +33 -0
- data/ext/src/lib.rs +79 -0
- data/lib/methodray/cli.rb +28 -0
- data/lib/methodray/commands.rb +78 -0
- data/lib/methodray/version.rb +5 -0
- data/lib/methodray.rb +9 -0
- data/rust/Cargo.toml +39 -0
- data/rust/src/analyzer/calls.rs +56 -0
- data/rust/src/analyzer/definitions.rs +70 -0
- data/rust/src/analyzer/dispatch.rs +134 -0
- data/rust/src/analyzer/install.rs +226 -0
- data/rust/src/analyzer/literals.rs +85 -0
- data/rust/src/analyzer/mod.rs +11 -0
- data/rust/src/analyzer/tests/integration_test.rs +136 -0
- data/rust/src/analyzer/tests/mod.rs +1 -0
- data/rust/src/analyzer/variables.rs +76 -0
- data/rust/src/cache/mod.rs +3 -0
- data/rust/src/cache/rbs_cache.rs +158 -0
- data/rust/src/checker.rs +139 -0
- data/rust/src/cli/args.rs +40 -0
- data/rust/src/cli/commands.rs +139 -0
- data/rust/src/cli/mod.rs +6 -0
- data/rust/src/diagnostics/diagnostic.rs +125 -0
- data/rust/src/diagnostics/formatter.rs +119 -0
- data/rust/src/diagnostics/mod.rs +5 -0
- data/rust/src/env/box_manager.rs +121 -0
- data/rust/src/env/global_env.rs +279 -0
- data/rust/src/env/local_env.rs +58 -0
- data/rust/src/env/method_registry.rs +63 -0
- data/rust/src/env/mod.rs +15 -0
- data/rust/src/env/scope.rs +330 -0
- data/rust/src/env/type_error.rs +23 -0
- data/rust/src/env/vertex_manager.rs +195 -0
- data/rust/src/graph/box.rs +157 -0
- data/rust/src/graph/change_set.rs +115 -0
- data/rust/src/graph/mod.rs +7 -0
- data/rust/src/graph/vertex.rs +167 -0
- data/rust/src/lib.rs +24 -0
- data/rust/src/lsp/diagnostics.rs +133 -0
- data/rust/src/lsp/main.rs +8 -0
- data/rust/src/lsp/mod.rs +4 -0
- data/rust/src/lsp/server.rs +138 -0
- data/rust/src/main.rs +46 -0
- data/rust/src/parser.rs +96 -0
- data/rust/src/rbs/converter.rs +82 -0
- data/rust/src/rbs/error.rs +37 -0
- data/rust/src/rbs/loader.rs +183 -0
- data/rust/src/rbs/mod.rs +15 -0
- data/rust/src/source_map.rs +102 -0
- data/rust/src/types.rs +75 -0
- 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
|
+
}
|
data/rust/src/checker.rs
ADDED
|
@@ -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
|
+
}
|
data/rust/src/cli/mod.rs
ADDED
|
@@ -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
|
+
}
|