diff --git a/Cargo.lock b/Cargo.lock index 235aaa86936..5d672c0f372 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1976,6 +1976,7 @@ dependencies = [ "hex", "ipfs-api-backend-hyper", "petgraph", + "regex", "reqwest", "semver", "serde", @@ -4989,14 +4990,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.5" +version = "1.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.3.8", - "regex-syntax 0.7.5", + "regex-automata 0.4.3", + "regex-syntax 0.8.2", ] [[package]] @@ -5013,10 +5014,16 @@ name = "regex-automata" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.7.5", + "regex-syntax 0.8.2", ] [[package]] @@ -5031,6 +5038,12 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + [[package]] name = "reqwest" version = "0.11.20" @@ -6089,6 +6102,7 @@ dependencies = [ "proc-macro2", "quote", "rayon", + "regex", "ropey", "serde", "serde_json", diff --git a/forc-pkg/Cargo.toml b/forc-pkg/Cargo.toml index dc85743c5a6..3d797984cc6 100644 --- a/forc-pkg/Cargo.toml +++ b/forc-pkg/Cargo.toml @@ -41,5 +41,8 @@ url = { version = "2.2", features = ["serde"] } vec1 = "1.8.0" walkdir = "2" +[dev-dependencies] +regex = "^1.10.2" + [target.'cfg(not(target_os = "macos"))'.dependencies] sysinfo = "0.29.0" diff --git a/forc-pkg/src/pkg.rs b/forc-pkg/src/pkg.rs index 3c51a8cba41..a0f9a21270f 100644 --- a/forc-pkg/src/pkg.rs +++ b/forc-pkg/src/pkg.rs @@ -11,7 +11,7 @@ use forc_util::{ }; use fuel_abi_types::program_abi; use petgraph::{ - self, + self, dot, visit::{Bfs, Dfs, EdgeRef, Walker}, Directed, Direction, }; @@ -826,6 +826,28 @@ impl BuildPlan { .flatten() }) } + + /// Returns a [String] representing the build dependency graph in GraphViz DOT format. + pub fn visualize(&self, url_file_prefix: Option) -> String { + format!( + "{:?}", + dot::Dot::with_attr_getters( + &self.graph, + &[dot::Config::NodeNoLabel, dot::Config::EdgeNoLabel], + &|_, _| "".to_string(), + &|_, nr| { + let url = url_file_prefix.clone().map_or("".to_string(), |prefix| { + self.manifest_map + .get(&nr.1.id()) + .map_or("".to_string(), |manifest| { + format!("URL = \"{}{}\"", prefix, manifest.path().to_string_lossy()) + }) + }); + format!("label = \"{}\" shape = box {url}", nr.1.name) + }, + ) + ) + } } /// Given a graph and the known project name retrieved from the manifest, produce an iterator @@ -2698,28 +2720,70 @@ pub fn fuel_core_not_running(node_url: &str) -> anyhow::Error { Error::msg(message) } -#[test] -fn test_root_pkg_order() { - let current_dir = env!("CARGO_MANIFEST_DIR"); - let manifest_dir = PathBuf::from(current_dir) - .parent() +#[cfg(test)] +mod test { + use super::*; + use regex::Regex; + + fn setup_build_plan() -> BuildPlan { + let current_dir = env!("CARGO_MANIFEST_DIR"); + let manifest_dir = PathBuf::from(current_dir) + .parent() + .unwrap() + .join("test/src/e2e_vm_tests/test_programs/should_pass/forc/workspace_building/"); + let manifest_file = ManifestFile::from_dir(&manifest_dir).unwrap(); + let member_manifests = manifest_file.member_manifests().unwrap(); + let lock_path = manifest_file.lock_path().unwrap(); + BuildPlan::from_lock_and_manifests( + &lock_path, + &member_manifests, + false, + false, + Default::default(), + ) .unwrap() - .join("test/src/e2e_vm_tests/test_programs/should_pass/forc/workspace_building/"); - let manifest_file = ManifestFile::from_dir(&manifest_dir).unwrap(); - let member_manifests = manifest_file.member_manifests().unwrap(); - let lock_path = manifest_file.lock_path().unwrap(); - let build_plan = BuildPlan::from_lock_and_manifests( - &lock_path, - &member_manifests, - false, - false, - Default::default(), - ) - .unwrap(); - let graph = build_plan.graph(); - let order: Vec = build_plan - .member_nodes() - .map(|order| graph[order].name.clone()) - .collect(); - assert_eq!(order, vec!["test_lib", "test_contract", "test_script"]) + } + + #[test] + fn test_root_pkg_order() { + let build_plan = setup_build_plan(); + let graph = build_plan.graph(); + let order: Vec = build_plan + .member_nodes() + .map(|order| graph[order].name.clone()) + .collect(); + assert_eq!(order, vec!["test_lib", "test_contract", "test_script"]) + } + + #[test] + fn test_visualize_with_url_prefix() { + let build_plan = setup_build_plan(); + let result = build_plan.visualize(Some("some-prefix::".to_string())); + let re = Regex::new(r#"digraph \{ + 0 \[ label = "test_contract" shape = box URL = "some-prefix::/[[:ascii:]]+/test_contract/Forc.toml"\] + 1 \[ label = "test_lib" shape = box URL = "some-prefix::/[[:ascii:]]+/test_lib/Forc.toml"\] + 2 \[ label = "test_script" shape = box URL = "some-prefix::/[[:ascii:]]+/test_script/Forc.toml"\] + 2 -> 1 \[ \] + 2 -> 0 \[ \] + 0 -> 1 \[ \] +\} +"#).unwrap(); + assert!(!re.find(result.as_str()).unwrap().is_empty()); + } + + #[test] + fn test_visualize_without_prefix() { + let build_plan = setup_build_plan(); + let result = build_plan.visualize(None); + let expected = r#"digraph { + 0 [ label = "test_contract" shape = box ] + 1 [ label = "test_lib" shape = box ] + 2 [ label = "test_script" shape = box ] + 2 -> 1 [ ] + 2 -> 0 [ ] + 0 -> 1 [ ] +} +"#; + assert_eq!(expected, result); + } } diff --git a/sway-lsp/Cargo.toml b/sway-lsp/Cargo.toml index 61bf5a0ed1d..3af61d8a1fe 100644 --- a/sway-lsp/Cargo.toml +++ b/sway-lsp/Cargo.toml @@ -58,6 +58,7 @@ futures = { version = "0.3", default-features = false, features = [ "async-await", ] } pretty_assertions = "1.4.0" +regex = "^1.10.2" sway-lsp-test-utils = { path = "tests/utils" } tower = { version = "0.4.12", default-features = false, features = ["util"] } diff --git a/sway-lsp/src/core/session.rs b/sway-lsp/src/core/session.rs index 6eebbcd5923..fd0ffa6c110 100644 --- a/sway-lsp/src/core/session.rs +++ b/sway-lsp/src/core/session.rs @@ -399,7 +399,7 @@ impl Session { } /// Create a [BuildPlan] from the given [Url] appropriate for the language server. -fn build_plan(uri: &Url) -> Result { +pub(crate) fn build_plan(uri: &Url) -> Result { let manifest_dir = PathBuf::from(uri.path()); let manifest = ManifestFile::from_dir(&manifest_dir).map_err(|_| DocumentError::ManifestFileNotFound { diff --git a/sway-lsp/src/handlers/request.rs b/sway-lsp/src/handlers/request.rs index 3d8e5fdfc31..59f46d6ad45 100644 --- a/sway-lsp/src/handlers/request.rs +++ b/sway-lsp/src/handlers/request.rs @@ -1,7 +1,9 @@ //! This module is responsible for implementing handlers for Language Server //! Protocol. This module specifically handles requests. -use crate::{capabilities, lsp_ext, server_state::ServerState, utils::debug}; +use crate::{ + capabilities, core::session::build_plan, lsp_ext, server_state::ServerState, utils::debug, +}; use forc_tracing::{init_tracing_subscriber, TracingSubscriberOptions, TracingWriterMode}; use lsp_types::{ CodeLens, CompletionResponse, DocumentFormattingParams, DocumentSymbolResponse, @@ -407,7 +409,7 @@ pub fn handle_show_ast( } /// This method is triggered when the use hits enter or pastes a newline in the editor. -pub(crate) fn on_enter( +pub(crate) fn handle_on_enter( state: &ServerState, params: lsp_ext::OnEnterParams, ) -> Result> { @@ -426,3 +428,22 @@ pub(crate) fn on_enter( } } } + +/// Returns a [String] of the GraphViz DOT representation of a graph. +pub fn handle_visualize( + _state: &ServerState, + params: lsp_ext::VisualizeParams, +) -> Result> { + match params.graph_kind.as_str() { + "build_plan" => match build_plan(¶ms.text_document.uri) { + Ok(build_plan) => Ok(Some( + build_plan.visualize(Some("vscode://file".to_string())), + )), + Err(err) => { + tracing::error!("{}", err.to_string()); + Ok(None) + } + }, + _ => Ok(None), + } +} diff --git a/sway-lsp/src/lib.rs b/sway-lsp/src/lib.rs index cd980bdf83c..4b7eddeddde 100644 --- a/sway-lsp/src/lib.rs +++ b/sway-lsp/src/lib.rs @@ -26,6 +26,7 @@ use tower_lsp::{LspService, Server}; pub async fn start() { let (service, socket) = LspService::build(ServerState::new) .custom_method("sway/show_ast", ServerState::show_ast) + .custom_method("sway/visualize", ServerState::visualize) .custom_method("sway/on_enter", ServerState::on_enter) .finish(); Server::new(tokio::io::stdin(), tokio::io::stdout(), socket) diff --git a/sway-lsp/src/lsp_ext.rs b/sway-lsp/src/lsp_ext.rs index 9407f2af38c..e13627a4ed7 100644 --- a/sway-lsp/src/lsp_ext.rs +++ b/sway-lsp/src/lsp_ext.rs @@ -18,3 +18,10 @@ pub struct OnEnterParams { /// The actual content changes, including the newline. pub content_changes: Vec, } + +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VisualizeParams { + pub text_document: TextDocumentIdentifier, + pub graph_kind: String, +} diff --git a/sway-lsp/src/server.rs b/sway-lsp/src/server.rs index a374c353917..9cde8406e66 100644 --- a/sway-lsp/src/server.rs +++ b/sway-lsp/src/server.rs @@ -4,7 +4,7 @@ use crate::{ core::document, handlers::{notification, request}, - lsp_ext::{OnEnterParams, ShowAstParams}, + lsp_ext::{OnEnterParams, ShowAstParams, VisualizeParams}, server_state::ServerState, }; use lsp_types::{ @@ -134,6 +134,10 @@ impl ServerState { } pub async fn on_enter(&self, params: OnEnterParams) -> Result> { - request::on_enter(self, params) + request::handle_on_enter(self, params) + } + + pub async fn visualize(&self, params: VisualizeParams) -> Result> { + request::handle_visualize(self, params) } } diff --git a/sway-lsp/tests/integration/lsp.rs b/sway-lsp/tests/integration/lsp.rs index dbd7b8ca15c..3b1be3daeb0 100644 --- a/sway-lsp/tests/integration/lsp.rs +++ b/sway-lsp/tests/integration/lsp.rs @@ -4,9 +4,14 @@ use crate::{GotoDefinition, HoverDocumentation, Rename}; use assert_json_diff::assert_json_eq; +use regex::Regex; use serde_json::json; use std::{borrow::Cow, path::Path}; -use sway_lsp::{handlers::request, lsp_ext::ShowAstParams, server_state::ServerState}; +use sway_lsp::{ + handlers::request, + lsp_ext::{ShowAstParams, VisualizeParams}, + server_state::ServerState, +}; use tower::{Service, ServiceExt}; use tower_lsp::{ jsonrpc::{Id, Request, Response}, @@ -141,6 +146,22 @@ pub(crate) async fn show_ast_request( assert_eq!(expected, response.unwrap().unwrap()); } +pub(crate) async fn visualize_request(server: &ServerState, uri: &Url, graph_kind: &str) { + let params = VisualizeParams { + text_document: TextDocumentIdentifier { uri: uri.clone() }, + graph_kind: graph_kind.to_string(), + }; + + let response = request::handle_visualize(server, params).unwrap().unwrap(); + let re = Regex::new(r#"digraph \{ + 0 \[ label = "core" shape = box URL = "vscode://file/[[:ascii:]]+/sway-lib-core/Forc.toml"\] + 1 \[ label = "struct_field_access" shape = box URL = "vscode://file/[[:ascii:]]+/struct_field_access/Forc.toml"\] + 1 -> 0 \[ \] +\} +"#).unwrap(); + assert!(!re.find(response.as_str()).unwrap().is_empty()); +} + pub(crate) fn semantic_tokens_request(server: &ServerState, uri: &Url) { let params = SemanticTokensParams { text_document: TextDocumentIdentifier { uri: uri.clone() }, diff --git a/sway-lsp/tests/lib.rs b/sway-lsp/tests/lib.rs index 3c36338d4d0..cc59fc773c1 100644 --- a/sway-lsp/tests/lib.rs +++ b/sway-lsp/tests/lib.rs @@ -123,6 +123,14 @@ async fn show_ast() { let _ = server.shutdown_server(); } +#[tokio::test] +async fn visualize() { + let server = ServerState::default(); + let uri = open(&server, e2e_test_dir().join("src/main.sw")).await; + lsp::visualize_request(&server, &uri, "build_plan").await; + let _ = server.shutdown_server(); +} + //------------------- GO TO DEFINITION -------------------// #[tokio::test]