From a831aa7982c8602e69b3d76526a63eea9d58bbec Mon Sep 17 00:00:00 2001 From: Jack Clayton Date: Wed, 12 Jan 2022 12:11:06 +0800 Subject: [PATCH] Move logic into main binary --- Cargo.toml | 4 - src/fix-rust-analyzer.rs | 72 ------------ src/fix_rust_analyzer.rs | 150 ++++++++++++++++++++++++ src/main.rs | 32 +++++ tests/fixture/failure/rust-project.json | 1 + tests/fixture/state/rust-project.json | 1 + tests/fixture/success/rust-project.json | 1 + 7 files changed, 185 insertions(+), 76 deletions(-) delete mode 100644 src/fix-rust-analyzer.rs create mode 100644 src/fix_rust_analyzer.rs create mode 100644 tests/fixture/failure/rust-project.json create mode 100644 tests/fixture/state/rust-project.json create mode 100644 tests/fixture/success/rust-project.json diff --git a/Cargo.toml b/Cargo.toml index 386066e3..6fa38c74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,10 +21,6 @@ home = "0.5.3" name = "rustlings" path = "src/main.rs" -[[bin]] -name = "fix-rust-analyzer" -path = "src/fix-rust-analyzer.rs" - [dev-dependencies] assert_cmd = "0.11.0" predicates = "1.0.1" diff --git a/src/fix-rust-analyzer.rs b/src/fix-rust-analyzer.rs deleted file mode 100644 index 3d408475..00000000 --- a/src/fix-rust-analyzer.rs +++ /dev/null @@ -1,72 +0,0 @@ -use glob::glob; -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize)] -struct RustProject { - sysroot_src: String, - crates: Vec, -} - -#[derive(Serialize, Deserialize)] -struct Crate { - root_module: String, - edition: String, - deps: Vec, -} - -impl RustProject { - fn new() -> RustProject { - RustProject { - sysroot_src: RustProject::get_sysroot_src(), - crates: Vec::new(), - } - } - - fn get_sysroot_src() -> String { - let mut sysroot_src = home::rustup_home() - .expect("Can't find Rustup... aborting") - .to_string_lossy() - .to_string(); - - use std::process::Command; - let output = Command::new("rustup") - .arg("default") - .output() - .expect("Failed to get rustup default toolchain"); - - let toolchain = String::from_utf8_lossy(&output.stdout).to_string(); - - sysroot_src += "/toolchains/"; - sysroot_src += toolchain - .split_once(' ') - .expect("Malformed default toolchain path") - .0; - sysroot_src += "/lib/rustlib/src/rust/library"; - println!("{}", sysroot_src); - sysroot_src - } -} - -fn main() { - let mut project = RustProject::new(); - for e in glob("./exercises/**/*").expect("Glob failed to read pattern") { - let path = e - .expect("Unable to extract path") - .to_string_lossy() - .to_string(); - if let Some((_, ext)) = path.split_once(".") { - if ext == "rs" { - project.crates.push(Crate { - deps: Vec::new(), - edition: "2021".to_string(), - root_module: path, - }) - } - } - } - std::fs::write( - "./rust-project.json", - serde_json::to_vec(&project).expect("Failed to serialize to JSON"), - ) - .expect("Failed to write file"); -} diff --git a/src/fix_rust_analyzer.rs b/src/fix_rust_analyzer.rs new file mode 100644 index 00000000..0f2dada9 --- /dev/null +++ b/src/fix_rust_analyzer.rs @@ -0,0 +1,150 @@ +/// because `rustlings` is a special type of project where we don't have a +/// cargo.toml linking to each exercise, we need a way to tell `rust-analyzer` +/// how to parse the exercises. This functionality is built into rust-analyzer +/// by putting a `rust-project.json` at the root of the repository. This module generates +/// that file by finding the default toolchain used and looping through each exercise +/// to build the configuration in a way that allows rust-analyzer to work with the exercises. +use glob::glob; +use serde::{Deserialize, Serialize}; +use std::error::Error; +use std::fmt; +use std::process::Command; + +/// Custom error to check if io error or rust-analyzer just doesn't exist +/// if rust-analyzer doesn't exist don't want to panic, want to print +/// message to console and continue +pub enum RustAnalyzerError { + IoError(std::io::Error), + NoRustAnalyzerError, +} + +impl fmt::Display for RustAnalyzerError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "invalid first item to double") + } +} + +impl RustAnalyzerError { + fn from_io(err: std::io::Error) -> RustAnalyzerError { + RustAnalyzerError::IoError(err) + } +} + +/// Contains the structure of resulting rust-project.json file +/// and functions to build the data required to create the file +#[derive(Serialize, Deserialize)] +pub struct RustAnalyzerProject { + sysroot_src: String, + crates: Vec, +} + +#[derive(Serialize, Deserialize)] +struct Crate { + root_module: String, + edition: String, + deps: Vec, +} + +impl RustAnalyzerProject { + pub fn new() -> RustAnalyzerProject { + RustAnalyzerProject { + sysroot_src: String::new(), + crates: Vec::new(), + } + } + + /// If path contains .rs extension, add a crate to `rust-project.json` + fn path_to_json(&mut self, path: String) { + if let Some((_, ext)) = path.split_once(".") { + if ext == "rs" { + self.crates.push(Crate { + root_module: path, + edition: "2021".to_string(), + deps: Vec::new(), + }) + } + } + } + + /// Write rust-project.json to disk + pub fn write_to_disk(&self) -> Result<(), std::io::Error> { + std::fs::write( + "./rust-project.json", + serde_json::to_vec(&self).expect("Failed to serialize to JSON"), + )?; + Ok(()) + } + + /// Parse the exercises folder for .rs files, any matches will create + /// a new `crate` in rust-project.json which allows rust-analyzer to + /// treat it like a normal binary + pub fn exercies_to_json(&mut self) -> Result<(), Box> { + let glob = glob("./exercises/**/*")?; + for e in glob { + let path = e?.to_string_lossy().to_string(); + self.path_to_json(path); + } + Ok(()) + } + + /// Run `rust-analyzer --version` to check if rust analyzer exists, if it doesn't + /// then return custom error + pub fn check_rust_analyzer_exists(&self) -> Result<(), RustAnalyzerError> { + match Command::new("rust-analyzer").arg("--version").output() { + Ok(out) => { + if out.stderr.len() > 0 { + return Err(RustAnalyzerError::NoRustAnalyzerError); + } else { + return Ok(()); + } + } + Err(err) => Err(RustAnalyzerError::from_io(err)), + } + } + + /// Use `rustup` command to determine the default toolchain, if it exists + /// it will be put in RustAnalyzerProject.sysroot_src, otherwise an error will be returned + pub fn get_sysroot_src(&mut self) -> Result<(), Box> { + let mut sysroot_src = home::rustup_home()?.to_string_lossy().to_string(); + + let output = Command::new("rustup").arg("default").output()?; + + let toolchain = String::from_utf8_lossy(&output.stdout).to_string(); + + sysroot_src += "/toolchains/"; + sysroot_src += toolchain + .split_once(' ') + .ok_or("Unable to determine toolchain")? + .0; + sysroot_src += "/lib/rustlib/src/rust/library"; + println!( + "Determined toolchain to use with Rustlings: {}", + sysroot_src + ); + self.sysroot_src = sysroot_src; + Ok(()) + } +} + +#[test] +fn parses_exercises() { + let mut rust_project = RustAnalyzerProject::new(); + rust_project + .exercies_to_json() + .expect("Failed to parse exercises"); + assert_eq!(rust_project.crates.len() > 0, true); +} + +#[test] +fn check_exists() { + let rust_project = RustAnalyzerProject::new(); + match rust_project.check_rust_analyzer_exists() { + Ok(_) => (), + Err(err) => match err { + RustAnalyzerError::NoRustAnalyzerError => { + println!("Correctly identifying rust-analyzer doesn't exist") + } + RustAnalyzerError::IoError(err) => panic!("io error: {}", err), + }, + } +} diff --git a/src/main.rs b/src/main.rs index 32e7bba2..5970ee3f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,5 @@ use crate::exercise::{Exercise, ExerciseList}; +use crate::fix_rust_analyzer::{RustAnalyzerError, RustAnalyzerProject}; use crate::run::run; use crate::verify::verify; use argh::FromArgs; @@ -20,6 +21,7 @@ use std::time::Duration; mod ui; mod exercise; +mod fix_rust_analyzer; mod run; mod verify; @@ -37,6 +39,9 @@ struct Args { version: bool, #[argh(subcommand)] nested: Option, + /// skip rust-analyzer fix check + #[argh(switch, short = 'x')] + skipfix: bool, } #[derive(FromArgs, PartialEq, Debug)] @@ -128,6 +133,10 @@ fn main() { std::process::exit(1); } + if !Path::new("rust-project.json").exists() && !args.skipfix { + fix_rust_analyzer(); + } + if !rustc_exists() { println!("We cannot find `rustc`."); println!("Try running `rustc --version` to diagnose your problem."); @@ -404,3 +413,26 @@ fn rustc_exists() -> bool { .map(|status| status.success()) .unwrap_or(false) } + +fn fix_rust_analyzer() { + let mut rust_project = RustAnalyzerProject::new(); + if let Err(err) = rust_project.check_rust_analyzer_exists() { + match err { + RustAnalyzerError::NoRustAnalyzerError => { + println!("rust-analyzer doesn't exist, skipping fix") + } + RustAnalyzerError::IoError(err) => { + println!("error trying to find rust-analyzer: {}", err) + } + } + } else { + println!("rust-analyzer exists, fixing to work with rustlings"); + if let Err(err) = rust_project.get_sysroot_src() { + println!("Error getting toolchain path: {}\nContinuing... rust-analyzer won't work with rustlings", &err) + } + if let Err(err) = rust_project.exercies_to_json() { + println!("Error parsing exercises and converting to json: {}\nContinuing... rust-analyzer won't work with rustlings", &err) + } + rust_project.write_to_disk(); + } +} diff --git a/tests/fixture/failure/rust-project.json b/tests/fixture/failure/rust-project.json new file mode 100644 index 00000000..f373cd74 --- /dev/null +++ b/tests/fixture/failure/rust-project.json @@ -0,0 +1 @@ +{"sysroot_src":"/home/jacko/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library","crates":[]} \ No newline at end of file diff --git a/tests/fixture/state/rust-project.json b/tests/fixture/state/rust-project.json new file mode 100644 index 00000000..f373cd74 --- /dev/null +++ b/tests/fixture/state/rust-project.json @@ -0,0 +1 @@ +{"sysroot_src":"/home/jacko/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library","crates":[]} \ No newline at end of file diff --git a/tests/fixture/success/rust-project.json b/tests/fixture/success/rust-project.json new file mode 100644 index 00000000..f373cd74 --- /dev/null +++ b/tests/fixture/success/rust-project.json @@ -0,0 +1 @@ +{"sysroot_src":"/home/jacko/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/library","crates":[]} \ No newline at end of file