From 00d927a1885ec2938d3365a8e136993445b437f5 Mon Sep 17 00:00:00 2001 From: David Wood Date: Tue, 5 Mar 2019 22:29:23 +0100 Subject: Initial implementation of project-lock.json. This commit adds a initial implementation of project-lock.json, a build system agnostic method of specifying the crate graph and roots. --- Cargo.lock | 2 + crates/ra_batch/src/lib.rs | 7 +- crates/ra_lsp_server/src/cargo_target_spec.rs | 20 +- crates/ra_lsp_server/src/server_world.rs | 9 +- crates/ra_lsp_server/tests/heavy_tests/main.rs | 65 +++++- crates/ra_lsp_server/tests/heavy_tests/support.rs | 6 +- crates/ra_project_model/Cargo.toml | 3 + crates/ra_project_model/src/json_project.rs | 49 +++++ crates/ra_project_model/src/lib.rs | 238 +++++++++++++++------- 9 files changed, 306 insertions(+), 93 deletions(-) create mode 100644 crates/ra_project_model/src/json_project.rs diff --git a/Cargo.lock b/Cargo.lock index 4551ffc44..4b89e36d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1089,6 +1089,8 @@ dependencies = [ "ra_arena 0.1.0", "ra_db 0.1.0", "rustc-hash 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", + "serde 1.0.89 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.39 (registry+https://github.com/rust-lang/crates.io-index)", "test_utils 0.1.0", "walkdir 2.2.7 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/crates/ra_batch/src/lib.rs b/crates/ra_batch/src/lib.rs index 89a234f4c..2db038063 100644 --- a/crates/ra_batch/src/lib.rs +++ b/crates/ra_batch/src/lib.rs @@ -99,12 +99,7 @@ impl BatchDatabase { let ws = ProjectWorkspace::discover(root.as_ref())?; let mut roots = Vec::new(); roots.push(root.clone()); - for pkg in ws.cargo.packages() { - roots.push(pkg.root(&ws.cargo).to_path_buf()); - } - for krate in ws.sysroot.crates() { - roots.push(krate.root_dir(&ws.sysroot).to_path_buf()) - } + ws.add_roots(&mut roots); let (mut vfs, roots) = Vfs::new(roots); let mut load = |path: &Path| { let vfs_file = vfs.load(path); diff --git a/crates/ra_lsp_server/src/cargo_target_spec.rs b/crates/ra_lsp_server/src/cargo_target_spec.rs index e011eab7c..cdf2ec10b 100644 --- a/crates/ra_lsp_server/src/cargo_target_spec.rs +++ b/crates/ra_lsp_server/src/cargo_target_spec.rs @@ -1,5 +1,5 @@ use crate::{ - project_model::TargetKind, + project_model::{self, TargetKind}, server_world::ServerWorld, Result }; @@ -65,14 +65,16 @@ impl CargoTargetSpec { }; let file_id = world.analysis().crate_root(crate_id)?; let path = world.vfs.read().file2path(ra_vfs::VfsFile(file_id.0.into())); - let res = world.workspaces.iter().find_map(|ws| { - let tgt = ws.cargo.target_by_root(&path)?; - let res = CargoTargetSpec { - package: tgt.package(&ws.cargo).name(&ws.cargo).to_string(), - target: tgt.name(&ws.cargo).to_string(), - target_kind: tgt.kind(&ws.cargo), - }; - Some(res) + let res = world.workspaces.iter().find_map(|ws| match ws { + project_model::ProjectWorkspace::Cargo { cargo, .. } => { + let tgt = cargo.target_by_root(&path)?; + Some(CargoTargetSpec { + package: tgt.package(&cargo).name(&cargo).to_string(), + target: tgt.name(&cargo).to_string(), + target_kind: tgt.kind(&cargo), + }) + } + project_model::ProjectWorkspace::Json { .. } => None, }); Ok(res) } diff --git a/crates/ra_lsp_server/src/server_world.rs b/crates/ra_lsp_server/src/server_world.rs index 4a68c019f..4625a26a7 100644 --- a/crates/ra_lsp_server/src/server_world.rs +++ b/crates/ra_lsp_server/src/server_world.rs @@ -40,12 +40,7 @@ impl ServerWorldState { let mut roots = Vec::new(); roots.push(root.clone()); for ws in workspaces.iter() { - for pkg in ws.cargo.packages() { - roots.push(pkg.root(&ws.cargo).to_path_buf()); - } - for krate in ws.sysroot.crates() { - roots.push(krate.root_dir(&ws.sysroot).to_path_buf()) - } + ws.add_roots(&mut roots); } let (mut vfs, roots) = Vfs::new(roots); let roots_to_scan = roots.len(); @@ -185,7 +180,7 @@ impl ServerWorld { } else { res.push_str("workspaces:\n"); for w in self.workspaces.iter() { - res += &format!("{} packages loaded\n", w.cargo.packages().count()); + res += &format!("{} packages loaded\n", w.count()); } } res.push_str("\nanalysis:\n"); diff --git a/crates/ra_lsp_server/tests/heavy_tests/main.rs b/crates/ra_lsp_server/tests/heavy_tests/main.rs index 1c099a78f..641f84cfc 100644 --- a/crates/ra_lsp_server/tests/heavy_tests/main.rs +++ b/crates/ra_lsp_server/tests/heavy_tests/main.rs @@ -12,8 +12,9 @@ use ra_lsp_server::req::{ CodeActionParams, CodeActionRequest, Formatting, Runnables, RunnablesParams, CompletionParams, Completion, }; use serde_json::json; +use tempfile::TempDir; -use crate::support::project; +use crate::support::{project, project_with_tmpdir}; const LOG: &'static str = ""; @@ -258,3 +259,65 @@ fn main() {} json!([]), ); } + +#[test] +fn test_missing_module_code_action_in_json_project() { + let tmp_dir = TempDir::new().unwrap(); + let code = format!( + r#" +//- rust-project.json +{{ + "roots": [ "{PATH}" ], + "crates": [ {{ "root_module": "{PATH}/src/lib.rs", "deps": [], "edition": "2015" }} ] +}} + +//- src/lib.rs +mod bar; + +fn main() {} +"#, + PATH = tmp_dir.path().display() + ); + let server = project_with_tmpdir(tmp_dir, &code); + server.wait_for_feedback("workspace loaded"); + let empty_context = || CodeActionContext { diagnostics: Vec::new(), only: None }; + server.request::( + CodeActionParams { + text_document: server.doc_id("src/lib.rs"), + range: Range::new(Position::new(0, 4), Position::new(0, 7)), + context: empty_context(), + }, + json!([ + { + "command": { + "arguments": [ + { + "cursorPosition": null, + "label": "create module", + "workspaceEdit": { + "documentChanges": [ + { + "kind": "create", + "uri": "file:///[..]/src/bar.rs" + } + ] + } + } + ], + "command": "rust-analyzer.applySourceChange", + "title": "create module" + }, + "title": "create module" + } + ]), + ); + + server.request::( + CodeActionParams { + text_document: server.doc_id("src/lib.rs"), + range: Range::new(Position::new(2, 4), Position::new(2, 7)), + context: empty_context(), + }, + json!([]), + ); +} diff --git a/crates/ra_lsp_server/tests/heavy_tests/support.rs b/crates/ra_lsp_server/tests/heavy_tests/support.rs index 8bfc8d622..e02d7858e 100644 --- a/crates/ra_lsp_server/tests/heavy_tests/support.rs +++ b/crates/ra_lsp_server/tests/heavy_tests/support.rs @@ -27,12 +27,16 @@ use ra_lsp_server::{ }; pub fn project(fixture: &str) -> Server { + let tmp_dir = TempDir::new().unwrap(); + project_with_tmpdir(tmp_dir, fixture) +} + +pub fn project_with_tmpdir(tmp_dir: TempDir, fixture: &str) -> Server { static INIT: Once = Once::new(); INIT.call_once(|| { let _ = Logger::with_env_or_str(crate::LOG).start().unwrap(); }); - let tmp_dir = TempDir::new().unwrap(); let mut paths = vec![]; for entry in parse_fixture(fixture) { diff --git a/crates/ra_project_model/Cargo.toml b/crates/ra_project_model/Cargo.toml index 262406e99..487cdfaf1 100644 --- a/crates/ra_project_model/Cargo.toml +++ b/crates/ra_project_model/Cargo.toml @@ -17,5 +17,8 @@ cargo_metadata = "0.7.0" ra_arena = { path = "../ra_arena" } ra_db = { path = "../ra_db" } +serde = "1.0.89" +serde_json = "1.0.39" + [dev-dependencies] test_utils = { path = "../test_utils" } diff --git a/crates/ra_project_model/src/json_project.rs b/crates/ra_project_model/src/json_project.rs new file mode 100644 index 000000000..9a9eb9e1f --- /dev/null +++ b/crates/ra_project_model/src/json_project.rs @@ -0,0 +1,49 @@ +use std::path::PathBuf; + +use serde::Deserialize; + +/// A root points to the directory which contains Rust crates. rust-analyzer watches all files in +/// all roots. Roots might be nested. +#[derive(Clone, Debug, Deserialize)] +#[serde(transparent)] +pub struct Root { + pub(crate) path: PathBuf, +} + +/// A crate points to the root module of a crate and lists the dependencies of the crate. This is +/// useful in creating the crate graph. +#[derive(Clone, Debug, Deserialize)] +pub struct Crate { + pub(crate) root_module: PathBuf, + pub(crate) edition: Edition, + pub(crate) deps: Vec, +} + +#[derive(Clone, Copy, Debug, Deserialize)] +#[serde(rename = "edition")] +pub enum Edition { + #[serde(rename = "2015")] + Edition2015, + #[serde(rename = "2018")] + Edition2018, +} + +/// Identifies a crate by position in the crates array. +#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd)] +#[serde(transparent)] +pub struct CrateId(pub usize); + +/// A dependency of a crate, identified by its id in the crates array and name. +#[derive(Clone, Debug, Deserialize)] +pub struct Dep { + #[serde(rename = "crate")] + pub(crate) krate: CrateId, + pub(crate) name: String, +} + +/// Roots and crates that compose this Rust project. +#[derive(Clone, Debug, Deserialize)] +pub struct JsonProject { + pub(crate) roots: Vec, + pub(crate) crates: Vec, +} diff --git a/crates/ra_project_model/src/lib.rs b/crates/ra_project_model/src/lib.rs index 1b18ac836..ded222446 100644 --- a/crates/ra_project_model/src/lib.rs +++ b/crates/ra_project_model/src/lib.rs @@ -1,15 +1,23 @@ mod cargo_workspace; +mod json_project; mod sysroot; -use std::path::{Path, PathBuf}; +use std::{ + fs::File, + io::BufReader, + path::{Path, PathBuf}, +}; use failure::bail; use rustc_hash::FxHashMap; use ra_db::{CrateGraph, FileId, Edition}; +use serde_json::from_reader; + pub use crate::{ cargo_workspace::{CargoWorkspace, Package, Target, TargetKind}, + json_project::JsonProject, sysroot::Sysroot, }; @@ -17,105 +25,197 @@ pub use crate::{ pub type Result = ::std::result::Result; #[derive(Debug, Clone)] -pub struct ProjectWorkspace { - pub cargo: CargoWorkspace, - pub sysroot: Sysroot, +pub enum ProjectWorkspace { + /// Project workspace was discovered by running `cargo metadata` and `rustc --print sysroot`. + Cargo { cargo: CargoWorkspace, sysroot: Sysroot }, + /// Project workspace was manually specified using a `rust-project.json` file. + Json { project: JsonProject }, } impl ProjectWorkspace { pub fn discover(path: &Path) -> Result { - let cargo_toml = find_cargo_toml(path)?; - let cargo = CargoWorkspace::from_cargo_metadata(&cargo_toml)?; - let sysroot = Sysroot::discover(&cargo_toml)?; - let res = ProjectWorkspace { cargo, sysroot }; - Ok(res) + match find_rust_project_json(path) { + Some(json_path) => { + let file = File::open(json_path)?; + let reader = BufReader::new(file); + Ok(ProjectWorkspace::Json { project: from_reader(reader)? }) + } + None => { + let cargo_toml = find_cargo_toml(path)?; + Ok(ProjectWorkspace::Cargo { + cargo: CargoWorkspace::from_cargo_metadata(&cargo_toml)?, + sysroot: Sysroot::discover(&cargo_toml)?, + }) + } + } } - pub fn to_crate_graph(&self, load: &mut dyn FnMut(&Path) -> Option) -> CrateGraph { - let mut crate_graph = CrateGraph::default(); - let mut sysroot_crates = FxHashMap::default(); - for krate in self.sysroot.crates() { - if let Some(file_id) = load(krate.root(&self.sysroot)) { - sysroot_crates - .insert(krate, crate_graph.add_crate_root(file_id, Edition::Edition2015)); + pub fn add_roots(&self, roots: &mut Vec) { + match self { + ProjectWorkspace::Json { project } => { + for root in &project.roots { + roots.push(root.path.clone()); + } } - } - for from in self.sysroot.crates() { - for to in from.deps(&self.sysroot) { - let name = to.name(&self.sysroot); - if let (Some(&from), Some(&to)) = - (sysroot_crates.get(&from), sysroot_crates.get(&to)) - { - if let Err(_) = crate_graph.add_dep(from, name.into(), to) { - log::error!("cyclic dependency between sysroot crates") - } + ProjectWorkspace::Cargo { cargo, sysroot } => { + for pkg in cargo.packages() { + roots.push(pkg.root(&cargo).to_path_buf()); + } + for krate in sysroot.crates() { + roots.push(krate.root_dir(&sysroot).to_path_buf()) } } } + } - let libstd = self.sysroot.std().and_then(|it| sysroot_crates.get(&it).map(|&it| it)); - - let mut pkg_to_lib_crate = FxHashMap::default(); - let mut pkg_crates = FxHashMap::default(); - // Next, create crates for each package, target pair - for pkg in self.cargo.packages() { - let mut lib_tgt = None; - for tgt in pkg.targets(&self.cargo) { - let root = tgt.root(&self.cargo); - if let Some(file_id) = load(root) { - let edition = pkg.edition(&self.cargo); - let crate_id = crate_graph.add_crate_root(file_id, edition); - if tgt.kind(&self.cargo) == TargetKind::Lib { - lib_tgt = Some(crate_id); - pkg_to_lib_crate.insert(pkg, crate_id); + pub fn count(&self) -> usize { + match self { + ProjectWorkspace::Json { project } => project.crates.len(), + ProjectWorkspace::Cargo { cargo, .. } => cargo.packages().count(), + } + } + + pub fn to_crate_graph(&self, load: &mut dyn FnMut(&Path) -> Option) -> CrateGraph { + let mut crate_graph = CrateGraph::default(); + match self { + ProjectWorkspace::Json { project } => { + let mut crates = FxHashMap::default(); + for (id, krate) in project.crates.iter().enumerate() { + let crate_id = json_project::CrateId(id); + if let Some(file_id) = load(&krate.root_module) { + let edition = match krate.edition { + json_project::Edition::Edition2015 => Edition::Edition2015, + json_project::Edition::Edition2018 => Edition::Edition2018, + }; + crates.insert(crate_id, crate_graph.add_crate_root(file_id, edition)); } - pkg_crates.entry(pkg).or_insert_with(Vec::new).push(crate_id); } - } - // Set deps to the std and to the lib target of the current package - for &from in pkg_crates.get(&pkg).into_iter().flatten() { - if let Some(to) = lib_tgt { - if to != from { - if let Err(_) = crate_graph.add_dep(from, pkg.name(&self.cargo).into(), to) + for (id, krate) in project.crates.iter().enumerate() { + for dep in &krate.deps { + let from_crate_id = json_project::CrateId(id); + let to_crate_id = dep.krate; + if let (Some(&from), Some(&to)) = + (crates.get(&from_crate_id), crates.get(&to_crate_id)) { - log::error!( - "cyclic dependency between targets of {}", - pkg.name(&self.cargo) - ) + if let Err(_) = crate_graph.add_dep(from, dep.name.clone().into(), to) { + log::error!( + "cyclic dependency {:?} -> {:?}", + from_crate_id, + to_crate_id + ); + } } } } - if let Some(std) = libstd { - if let Err(_) = crate_graph.add_dep(from, "std".into(), std) { - log::error!("cyclic dependency on std for {}", pkg.name(&self.cargo)) + } + ProjectWorkspace::Cargo { cargo, sysroot } => { + let mut sysroot_crates = FxHashMap::default(); + for krate in sysroot.crates() { + if let Some(file_id) = load(krate.root(&sysroot)) { + sysroot_crates.insert( + krate, + crate_graph.add_crate_root(file_id, Edition::Edition2015), + ); } } - } - } + for from in sysroot.crates() { + for to in from.deps(&sysroot) { + let name = to.name(&sysroot); + if let (Some(&from), Some(&to)) = + (sysroot_crates.get(&from), sysroot_crates.get(&to)) + { + if let Err(_) = crate_graph.add_dep(from, name.into(), to) { + log::error!("cyclic dependency between sysroot crates") + } + } + } + } + + let libstd = sysroot.std().and_then(|it| sysroot_crates.get(&it).map(|&it| it)); + + let mut pkg_to_lib_crate = FxHashMap::default(); + let mut pkg_crates = FxHashMap::default(); + // Next, create crates for each package, target pair + for pkg in cargo.packages() { + let mut lib_tgt = None; + for tgt in pkg.targets(&cargo) { + let root = tgt.root(&cargo); + if let Some(file_id) = load(root) { + let edition = pkg.edition(&cargo); + let crate_id = crate_graph.add_crate_root(file_id, edition); + if tgt.kind(&cargo) == TargetKind::Lib { + lib_tgt = Some(crate_id); + pkg_to_lib_crate.insert(pkg, crate_id); + } + pkg_crates.entry(pkg).or_insert_with(Vec::new).push(crate_id); + } + } - // Now add a dep ednge from all targets of upstream to the lib - // target of downstream. - for pkg in self.cargo.packages() { - for dep in pkg.dependencies(&self.cargo) { - if let Some(&to) = pkg_to_lib_crate.get(&dep.pkg) { + // Set deps to the std and to the lib target of the current package for &from in pkg_crates.get(&pkg).into_iter().flatten() { - if let Err(_) = crate_graph.add_dep(from, dep.name.clone().into(), to) { - log::error!( - "cyclic dependency {} -> {}", - pkg.name(&self.cargo), - dep.pkg.name(&self.cargo) - ) + if let Some(to) = lib_tgt { + if to != from { + if let Err(_) = + crate_graph.add_dep(from, pkg.name(&cargo).into(), to) + { + log::error!( + "cyclic dependency between targets of {}", + pkg.name(&cargo) + ) + } + } + } + if let Some(std) = libstd { + if let Err(_) = crate_graph.add_dep(from, "std".into(), std) { + log::error!("cyclic dependency on std for {}", pkg.name(&cargo)) + } + } + } + } + + // Now add a dep ednge from all targets of upstream to the lib + // target of downstream. + for pkg in cargo.packages() { + for dep in pkg.dependencies(&cargo) { + if let Some(&to) = pkg_to_lib_crate.get(&dep.pkg) { + for &from in pkg_crates.get(&pkg).into_iter().flatten() { + if let Err(_) = + crate_graph.add_dep(from, dep.name.clone().into(), to) + { + log::error!( + "cyclic dependency {} -> {}", + pkg.name(&cargo), + dep.pkg.name(&cargo) + ) + } + } } } } } } - crate_graph } } +fn find_rust_project_json(path: &Path) -> Option { + if path.ends_with("rust-project.json") { + return Some(path.to_path_buf()); + } + + let mut curr = Some(path); + while let Some(path) = curr { + let candidate = path.join("rust-project.json"); + if candidate.exists() { + return Some(candidate); + } + curr = path.parent(); + } + + None +} + fn find_cargo_toml(path: &Path) -> Result { if path.ends_with("Cargo.toml") { return Ok(path.to_path_buf()); -- cgit v1.2.3