perspt_core/plugin/
rust.rs1use super::*;
2
3pub struct RustPlugin;
5
6impl LanguagePlugin for RustPlugin {
7 fn name(&self) -> &str {
8 "rust"
9 }
10
11 fn extensions(&self) -> &[&str] {
12 &["rs"]
13 }
14
15 fn key_files(&self) -> &[&str] {
16 &["Cargo.toml", "Cargo.lock"]
17 }
18
19 fn required_binaries(&self) -> Vec<(&str, &str, &str)> {
20 vec![
21 ("cargo", "build/init", "Install Rust via https://rustup.rs"),
22 ("rustc", "compiler", "Install Rust via https://rustup.rs"),
23 (
24 "rust-analyzer",
25 "language server",
26 "rustup component add rust-analyzer",
27 ),
28 ]
29 }
30
31 fn get_lsp_config(&self) -> LspConfig {
32 LspConfig {
33 server_binary: "rust-analyzer".to_string(),
34 args: vec![],
35 language_id: "rust".to_string(),
36 }
37 }
38
39 fn get_init_action(&self, opts: &InitOptions) -> ProjectAction {
40 let command = if opts.is_empty_dir || opts.name == "." || opts.name == "./" {
41 "cargo init .".to_string()
42 } else {
43 format!("cargo new {}", opts.name)
44 };
45 ProjectAction::ExecCommand {
46 command,
47 description: "Initialize Rust project with Cargo".to_string(),
48 }
49 }
50
51 fn check_tooling_action(&self, path: &Path) -> ProjectAction {
52 if !path.join("Cargo.lock").exists() && path.join("Cargo.toml").exists() {
54 ProjectAction::ExecCommand {
55 command: "cargo fetch".to_string(),
56 description: "Fetch Rust dependencies".to_string(),
57 }
58 } else {
59 ProjectAction::NoAction
60 }
61 }
62
63 fn init_command(&self, opts: &InitOptions) -> String {
64 if opts.name == "." || opts.name == "./" {
65 "cargo init .".to_string()
66 } else {
67 format!("cargo new {}", opts.name)
68 }
69 }
70
71 fn test_command(&self) -> String {
72 "cargo test".to_string()
73 }
74
75 fn test_command_with_filter(&self, filter: Option<&str>) -> String {
76 match filter {
77 Some(filter) => format!("cargo test {filter}"),
78 None => self.test_command(),
79 }
80 }
81
82 fn run_command(&self) -> String {
83 "cargo run".to_string()
84 }
85
86 fn syntax_check_command(&self) -> Option<String> {
89 Some("cargo check --message-format=json".to_string())
93 }
94
95 fn build_command(&self) -> Option<String> {
96 Some("cargo build --message-format=json".to_string())
97 }
98
99 fn lint_command(&self) -> Option<String> {
100 Some("cargo clippy -- -D warnings".to_string())
101 }
102
103 fn file_ownership_patterns(&self) -> &[&str] {
104 &["rs", "Cargo.toml"]
105 }
106
107 fn is_test_file(&self, path: &str) -> bool {
108 let normalized = path.replace('\\', "/");
109 let file = normalized.rsplit('/').next().unwrap_or(&normalized);
110 normalized.starts_with("tests/")
111 || normalized.contains("/tests/")
112 || file.ends_with("_test.rs")
113 }
114
115 fn host_tool_available(&self) -> bool {
116 host_binary_available("cargo")
117 }
118
119 fn format_command(&self) -> Option<String> {
120 Some("cargo fmt".to_string())
121 }
122
123 fn format_check_command(&self) -> Option<String> {
124 Some("cargo fmt --check".to_string())
125 }
126
127 fn verifier_profile(&self) -> VerifierProfile {
128 let cargo = host_binary_available("cargo");
129 let clippy = cargo; let capabilities = vec![
132 VerifierCapability {
133 stage: VerifierStage::SyntaxCheck,
134 command: Some("cargo check --message-format=json".to_string()),
135 available: cargo,
136 fallback_command: None,
137 fallback_available: false,
138 },
139 VerifierCapability {
140 stage: VerifierStage::Build,
141 command: Some("cargo build --message-format=json".to_string()),
142 available: cargo,
143 fallback_command: None,
144 fallback_available: false,
145 },
146 VerifierCapability {
147 stage: VerifierStage::Test,
148 command: Some("cargo test".to_string()),
149 available: cargo,
150 fallback_command: None,
151 fallback_available: false,
152 },
153 VerifierCapability {
154 stage: VerifierStage::Lint,
155 command: Some("cargo clippy -- -D warnings".to_string()),
156 available: clippy,
157 fallback_command: None,
158 fallback_available: false,
159 },
160 VerifierCapability {
161 stage: VerifierStage::Format,
162 command: self.format_check_command(),
163 available: cargo,
164 fallback_command: None,
165 fallback_available: false,
166 },
167 ];
168
169 let primary = self.get_lsp_config();
170 let primary_available = host_binary_available(&primary.server_binary);
171
172 VerifierProfile {
173 plugin_name: self.name().to_string(),
174 capabilities,
175 lsp: LspCapability {
176 primary,
177 primary_available,
178 fallback: None,
179 fallback_available: false,
180 },
181 }
182 }
183
184 fn legal_support_files(&self) -> &[&str] {
187 &["Cargo.toml", "build.rs"]
188 }
189
190 fn manifest_mutation_policy(
191 &self,
192 manifest_path: &str,
193 ) -> crate::types::ManifestMutationPolicy {
194 if manifest_path == "Cargo.toml" {
196 crate::types::ManifestMutationPolicy::Deny
198 } else {
199 crate::types::ManifestMutationPolicy::Allow
200 }
201 }
202
203 fn dependency_commands(
204 &self,
205 action: crate::plugin::DependencyAction,
206 packages: &[String],
207 dev: bool,
208 ) -> Vec<String> {
209 use crate::plugin::DependencyAction;
210 let list = packages.join(" ");
211 match action {
212 DependencyAction::Add if dev => vec![format!("cargo add --dev {list}")],
213 DependencyAction::Add => vec![format!("cargo add {list}")],
214 DependencyAction::Remove => vec![format!("cargo remove {list}")],
215 DependencyAction::Update => vec![format!("cargo update {list}")],
216 }
217 }
218
219 fn dependency_files(&self) -> Vec<String> {
220 vec!["Cargo.toml".into(), "Cargo.lock".into()]
221 }
222
223 fn dependency_command_policy(&self, command: &str) -> crate::types::CommandPolicyDecision {
224 let trimmed = command.trim();
225 if trimmed.starts_with("cargo add ")
226 || trimmed.starts_with("cargo install ")
227 || trimmed.starts_with("cargo fetch")
228 {
229 crate::types::CommandPolicyDecision::Allow
230 } else if trimmed.starts_with("cargo remove ") {
231 crate::types::CommandPolicyDecision::RequireApproval
232 } else if trimmed.starts_with("cargo ") {
233 crate::types::CommandPolicyDecision::Allow
235 } else {
236 crate::types::CommandPolicyDecision::Deny
237 }
238 }
239
240 fn test_file_patterns(&self) -> &[&str] {
241 &["tests/*.rs", "tests/**/*.rs", "**/tests.rs"]
242 }
243}