diff --git a/Cargo.lock b/Cargo.lock index 2d921cf24aa..3bbc90f29d9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3637,6 +3637,8 @@ version = "0.16.1" dependencies = [ "async-trait", "dashmap 5.3.4", + "forc", + "forc-pkg", "forc-util", "futures", "ropey", diff --git a/docs/src/SUMMARY.md b/docs/src/SUMMARY.md index 1b563884e9b..7398a95bd9b 100644 --- a/docs/src/SUMMARY.md +++ b/docs/src/SUMMARY.md @@ -56,6 +56,7 @@ - [Commands](./forc/commands/index.md) - [forc addr2line](./forc/commands/forc_addr2line.md) - [forc build](./forc/commands/forc_build.md) + - [forc check](./forc/commands/forc_check.md) - [forc clean](./forc/commands/forc_clean.md) - [forc completions](./forc/commands/forc_completions.md) - [forc deploy](./forc/commands/forc_deploy.md) diff --git a/docs/src/forc/commands/forc_check.md b/docs/src/forc/commands/forc_check.md new file mode 100644 index 00000000000..49f24c6bc70 --- /dev/null +++ b/docs/src/forc/commands/forc_check.md @@ -0,0 +1 @@ +# forc check diff --git a/forc-pkg/src/pkg.rs b/forc-pkg/src/pkg.rs index f77ebdbc60d..be841dfb7b7 100644 --- a/forc-pkg/src/pkg.rs +++ b/forc-pkg/src/pkg.rs @@ -16,7 +16,7 @@ use petgraph::{ use serde::{Deserialize, Serialize}; use std::{ collections::{hash_map, BTreeSet, HashMap, HashSet}, - fmt, + fmt, fs, hash::{Hash, Hasher}, path::{Path, PathBuf}, str::FromStr, @@ -235,6 +235,68 @@ impl BuildPlan { }) } + /// Create a new build plan taking into account the state of both the Manifest and the existing lock file if there is one. + /// + /// This will first attempt to load a build plan from the lock file and validate the resulting graph using the current state of the Manifest. + /// + /// This includes checking if the [dependencies] or [patch] tables have changed and checking the validity of the local path dependencies. + /// If any changes are detected, the graph is updated and any new packages that require fetching are fetched. + /// + /// The resulting build plan should always be in a valid state that is ready for building or checking. + pub fn load_from_manifest( + manifest: &ManifestFile, + locked: bool, + offline: bool, + sway_git_tag: &str, + ) -> Result { + let lock_path = forc_util::lock_path(manifest.dir()); + let plan_result = BuildPlan::from_lock_file(&lock_path, sway_git_tag); + + // Retrieve the old lock file state so we can produce a diff. + let old_lock = plan_result + .as_ref() + .ok() + .map(|plan| Lock::from_graph(plan.graph())) + .unwrap_or_default(); + + // Check if there are any errors coming from the BuildPlan generation from the lock file + // If there are errors we will need to create the BuildPlan from scratch, i.e fetch & pin everything + let mut new_lock_cause = None; + let mut plan = plan_result.or_else(|e| -> Result { + if locked { + bail!( + "The lock file {} needs to be updated but --locked was passed to prevent this.", + lock_path.to_string_lossy() + ); + } + new_lock_cause = if e.to_string().contains("No such file or directory") { + Some(anyhow!("lock file did not exist")) + } else { + Some(e) + }; + let plan = BuildPlan::new(manifest, sway_git_tag, offline)?; + Ok(plan) + })?; + + // If there are no issues with the BuildPlan generated from the lock file + // Check and apply the diff. + if new_lock_cause.is_none() { + let diff = plan.validate(manifest, sway_git_tag)?; + if !diff.added.is_empty() || !diff.removed.is_empty() { + new_lock_cause = Some(anyhow!("lock file did not match manifest `diff`")); + plan = plan.apply_pkg_diff(diff, sway_git_tag, offline)?; + } + } + + if let Some(cause) = new_lock_cause { + info!(" Creating a new `Forc.lock` file. (Cause: {})", cause); + create_new_lock(&plan, &old_lock, manifest, &lock_path)?; + info!(" Created new lock file at {}", lock_path.display()); + } + + Ok(plan) + } + /// Create a new build plan from an existing one. Needs the difference with the existing plan with the lock. pub fn apply_pkg_diff( &self, @@ -1316,6 +1378,19 @@ pub fn dependency_namespace( namespace } +/// Compiles the package to an AST. +pub fn compile_ast( + manifest: &ManifestFile, + build_config: &BuildConfig, + namespace: namespace::Module, +) -> Result { + let source = manifest.entry_string()?; + let sway_build_config = + sway_build_config(manifest.dir(), &manifest.entry_path(), build_config)?; + let ast_res = sway_core::compile_to_ast(source, namespace, Some(&sway_build_config)); + Ok(ast_res) +} + /// Compiles the given package. /// /// ## Program Types @@ -1342,12 +1417,11 @@ pub fn compile( source_map: &mut SourceMap, ) -> Result<(Compiled, Option)> { let entry_path = manifest.entry_path(); - let source = manifest.entry_string()?; let sway_build_config = sway_build_config(manifest.dir(), &entry_path, build_config)?; let silent_mode = build_config.silent; // First, compile to an AST. We'll update the namespace and check for JSON ABI output. - let ast_res = sway_core::compile_to_ast(source, namespace, Some(&sway_build_config)); + let ast_res = compile_ast(manifest, build_config, namespace)?; match &ast_res { CompileAstResult::Failure { warnings, errors } => { print_on_failure(silent_mode, warnings, errors); @@ -1444,6 +1518,43 @@ pub fn build( Ok((compiled, source_map)) } +/// Compile the entire forc package and return a CompileAstResult. +pub fn check( + plan: &BuildPlan, + silent_mode: bool, + sway_git_tag: &str, +) -> anyhow::Result { + let conf = &BuildConfig { + print_ir: false, + print_finalized_asm: false, + print_intermediate_asm: false, + silent: silent_mode, + }; + + let mut namespace_map = Default::default(); + let mut source_map = SourceMap::new(); + for (i, &node) in plan.compilation_order.iter().enumerate() { + let dep_namespace = + dependency_namespace(&namespace_map, &plan.graph, &plan.compilation_order, node); + let pkg = &plan.graph[node]; + let path = &plan.path_map[&pkg.id()]; + let manifest = ManifestFile::from_dir(path, sway_git_tag)?; + let ast_res = compile_ast(&manifest, conf, dep_namespace)?; + if let CompileAstResult::Success { typed_program, .. } = &ast_res { + if let TreeType::Library { .. } = typed_program.kind.tree_type() { + namespace_map.insert(node, typed_program.root.namespace.clone()); + } + } + source_map.insert_dependency(path.clone()); + + // We only need to return the final CompileAstResult + if i == plan.compilation_order.len() - 1 { + return Ok(ast_res); + } + } + bail!("unable to check sway program: build plan contains no packages") +} + /// Attempt to find a `Forc.toml` with the given project name within the given directory. /// /// Returns the path to the package on success, or `None` in the case it could not be found. @@ -1565,3 +1676,18 @@ pub fn fuel_core_not_running(node_url: &str) -> anyhow::Error { let message = format!("could not get a response from node at the URL {}. Start a node with `fuel-core`. See https://github.com/FuelLabs/fuel-core#running for more information", node_url); Error::msg(message) } + +fn create_new_lock( + plan: &BuildPlan, + old_lock: &Lock, + manifest: &ManifestFile, + lock_path: &Path, +) -> Result<()> { + let lock = Lock::from_graph(plan.graph()); + let diff = lock.diff(old_lock); + super::lock::print_diff(&manifest.project.name, &diff); + let string = toml::ser::to_string_pretty(&lock) + .map_err(|e| anyhow!("failed to serialize lock file: {}", e))?; + fs::write(&lock_path, &string).map_err(|e| anyhow!("failed to write lock file: {}", e))?; + Ok(()) +} diff --git a/forc/src/cli/commands/check.rs b/forc/src/cli/commands/check.rs new file mode 100644 index 00000000000..94dc949b33b --- /dev/null +++ b/forc/src/cli/commands/check.rs @@ -0,0 +1,30 @@ +use crate::ops::forc_check; +use anyhow::Result; +use clap::Parser; + +/// Check the current or target project and all of its dependencies for errors. +/// +/// This will essentially compile the packages without performing the final step of code generation, +/// which is faster than running forc build. +#[derive(Debug, Default, Parser)] +pub struct Command { + /// Path to the project, if not specified, current working directory will be used. + #[clap(short, long)] + pub path: Option, + /// Offline mode, prevents Forc from using the network when managing dependencies. + /// Meaning it will only try to use previously downloaded dependencies. + #[clap(long = "offline")] + pub offline_mode: bool, + /// Silent mode. Don't output any warnings or errors to the command line. + #[clap(long = "silent", short = 's')] + pub silent_mode: bool, + /// Requires that the Forc.lock file is up-to-date. If the lock file is missing, or it + /// needs to be updated, Forc will exit with an error + #[clap(long)] + pub locked: bool, +} + +pub(crate) fn exec(command: Command) -> Result<()> { + forc_check::check(command)?; + Ok(()) +} diff --git a/forc/src/cli/commands/mod.rs b/forc/src/cli/commands/mod.rs index 90e3bc394ba..987b2d27965 100644 --- a/forc/src/cli/commands/mod.rs +++ b/forc/src/cli/commands/mod.rs @@ -1,5 +1,6 @@ pub mod addr2line; pub mod build; +pub mod check; pub mod clean; pub mod completions; pub mod deploy; diff --git a/forc/src/cli/mod.rs b/forc/src/cli/mod.rs index c4e6139014d..2a4fddce2cb 100644 --- a/forc/src/cli/mod.rs +++ b/forc/src/cli/mod.rs @@ -1,10 +1,11 @@ use self::commands::{ - addr2line, build, clean, completions, deploy, init, json_abi, parse_bytecode, plugins, run, - template, test, update, + addr2line, build, check, clean, completions, deploy, init, json_abi, parse_bytecode, plugins, + run, template, test, update, }; use addr2line::Command as Addr2LineCommand; use anyhow::{anyhow, Result}; pub use build::Command as BuildCommand; +pub use check::Command as CheckCommand; use clap::Parser; pub use clean::Command as CleanCommand; pub use completions::Command as CompletionsCommand; @@ -35,6 +36,7 @@ enum Forc { Addr2Line(Addr2LineCommand), #[clap(visible_alias = "b")] Build(BuildCommand), + Check(CheckCommand), Clean(CleanCommand), Completions(CompletionsCommand), Deploy(DeployCommand), @@ -64,6 +66,7 @@ pub async fn run_cli() -> Result<()> { match opt.command { Forc::Addr2Line(command) => addr2line::exec(command), Forc::Build(command) => build::exec(command), + Forc::Check(command) => check::exec(command), Forc::Clean(command) => clean::exec(command), Forc::Completions(command) => completions::exec(command), Forc::Deploy(command) => deploy::exec(command).await, diff --git a/forc/src/lib.rs b/forc/src/lib.rs index 61e0bf2f81a..3bd176db218 100644 --- a/forc/src/lib.rs +++ b/forc/src/lib.rs @@ -1,6 +1,6 @@ pub mod cli; mod ops; -mod utils; +pub mod utils; #[cfg(feature = "test")] pub mod test { diff --git a/forc/src/ops/forc_build.rs b/forc/src/ops/forc_build.rs index de910ef738a..76ca48d1ba0 100644 --- a/forc/src/ops/forc_build.rs +++ b/forc/src/ops/forc_build.rs @@ -2,13 +2,13 @@ use crate::{ cli::BuildCommand, utils::{SWAY_BIN_HASH_SUFFIX, SWAY_BIN_ROOT_SUFFIX, SWAY_GIT_TAG}, }; -use anyhow::{anyhow, bail, Result}; -use forc_pkg::{self as pkg, lock, Lock, ManifestFile}; -use forc_util::{default_output_directory, lock_path}; +use anyhow::Result; +use forc_pkg::{self as pkg, ManifestFile}; +use forc_util::default_output_directory; use fuel_tx::Contract; use std::{ fs::{self, File}, - path::{Path, PathBuf}, + path::PathBuf, }; use sway_core::TreeType; use tracing::{info, warn}; @@ -56,6 +56,8 @@ pub fn build(command: BuildCommand) -> Result { let manifest = ManifestFile::from_dir(&this_dir, SWAY_GIT_TAG)?; + let plan = pkg::BuildPlan::load_from_manifest(&manifest, locked, offline, SWAY_GIT_TAG)?; + // If any cli parameter is passed by the user it overrides the selected build profile. let mut config = &pkg::BuildConfig { print_ir, @@ -80,52 +82,6 @@ pub fn build(command: BuildCommand) -> Result { }); } - let lock_path = lock_path(manifest.dir()); - - let plan_result = pkg::BuildPlan::from_lock_file(&lock_path, SWAY_GIT_TAG); - - // Retrieve the old lock file state so we can produce a diff. - let old_lock = plan_result - .as_ref() - .ok() - .map(|plan| Lock::from_graph(plan.graph())) - .unwrap_or_default(); - - // Check if there are any errors coming from the BuildPlan generation from the lock file - // If there are errors we will need to create the BuildPlan from scratch, i.e fetch & pin everything - let mut new_lock_cause = None; - let mut plan = plan_result.or_else(|e| -> Result { - if locked { - bail!( - "The lock file {} needs to be updated but --locked was passed to prevent this.", - lock_path.to_string_lossy() - ); - } - new_lock_cause = if e.to_string().contains("No such file or directory") { - Some(anyhow!("lock file did not exist")) - } else { - Some(e) - }; - let plan = pkg::BuildPlan::new(&manifest, SWAY_GIT_TAG, offline)?; - Ok(plan) - })?; - - // If there are no issues with the BuildPlan generated from the lock file - // Check and apply the diff. - if new_lock_cause.is_none() { - let diff = plan.validate(&manifest, SWAY_GIT_TAG)?; - if !diff.added.is_empty() || !diff.removed.is_empty() { - new_lock_cause = Some(anyhow!("lock file did not match manifest `diff`")); - plan = plan.apply_pkg_diff(diff, SWAY_GIT_TAG, offline)?; - } - } - - if let Some(cause) = new_lock_cause { - info!(" Creating a new `Forc.lock` file. (Cause: {})", cause); - create_new_lock(&plan, &old_lock, &manifest, &lock_path)?; - info!(" Created new lock file at {}", lock_path.display()); - } - // Build it! let (compiled, source_map) = pkg::build(&plan, config, SWAY_GIT_TAG)?; @@ -187,18 +143,3 @@ pub fn build(command: BuildCommand) -> Result { Ok(compiled) } - -fn create_new_lock( - plan: &pkg::BuildPlan, - old_lock: &Lock, - manifest: &ManifestFile, - lock_path: &Path, -) -> Result<()> { - let lock = Lock::from_graph(plan.graph()); - let diff = lock.diff(old_lock); - lock::print_diff(&manifest.project.name, &diff); - let string = toml::ser::to_string_pretty(&lock) - .map_err(|e| anyhow!("failed to serialize lock file: {}", e))?; - fs::write(&lock_path, &string).map_err(|e| anyhow!("failed to write lock file: {}", e))?; - Ok(()) -} diff --git a/forc/src/ops/forc_check.rs b/forc/src/ops/forc_check.rs new file mode 100644 index 00000000000..be39de30508 --- /dev/null +++ b/forc/src/ops/forc_check.rs @@ -0,0 +1,23 @@ +use crate::{cli::CheckCommand, utils::SWAY_GIT_TAG}; +use anyhow::Result; +use forc_pkg::{self as pkg, ManifestFile}; +use std::path::PathBuf; + +pub fn check(command: CheckCommand) -> Result { + let CheckCommand { + path, + offline_mode: offline, + silent_mode, + locked, + } = command; + + let this_dir = if let Some(ref path) = path { + PathBuf::from(path) + } else { + std::env::current_dir()? + }; + let manifest = ManifestFile::from_dir(&this_dir, SWAY_GIT_TAG)?; + let plan = pkg::BuildPlan::load_from_manifest(&manifest, locked, offline, SWAY_GIT_TAG)?; + + pkg::check(&plan, silent_mode, SWAY_GIT_TAG) +} diff --git a/forc/src/ops/mod.rs b/forc/src/ops/mod.rs index 1d5acc42f28..2f5f87527db 100644 --- a/forc/src/ops/mod.rs +++ b/forc/src/ops/mod.rs @@ -1,5 +1,6 @@ pub mod forc_abi_json; pub mod forc_build; +pub mod forc_check; pub mod forc_clean; pub mod forc_deploy; pub mod forc_init; diff --git a/sway-lsp/Cargo.toml b/sway-lsp/Cargo.toml index 282b9975af0..24d880c2266 100644 --- a/sway-lsp/Cargo.toml +++ b/sway-lsp/Cargo.toml @@ -10,6 +10,8 @@ description = "LSP server for Sway." [dependencies] dashmap = "5.3.4" +forc = { version = "0.16.1", path = "../forc" } +forc-pkg = { version = "0.16.1", path = "../forc-pkg" } forc-util = { version = "0.16.1", path = "../forc-util" } ropey = "1.2" serde_json = "1.0.60" diff --git a/sway-lsp/src/core/document.rs b/sway-lsp/src/core/document.rs index 8137bd89fec..3eea41621a5 100644 --- a/sway-lsp/src/core/document.rs +++ b/sway-lsp/src/core/document.rs @@ -6,14 +6,10 @@ use super::traverse_typed_tree; use super::typed_token_type::TokenMap; use crate::{capabilities, core::token::traverse_node, utils}; +use forc_pkg::{self as pkg}; use ropey::Rope; -use std::collections::HashMap; -use std::sync::Arc; -use sway_core::{ - parse, - semantic_analysis::{ast_node::TypedAstNode, namespace}, - CompileAstResult, TreeType, -}; +use std::{collections::HashMap, path::PathBuf, sync::Arc}; +use sway_core::{parse, semantic_analysis::ast_node::TypedAstNode, CompileAstResult, TreeType}; use tower_lsp::lsp_types::{Diagnostic, Position, Range, TextDocumentContentChangeEvent}; #[derive(Debug)] @@ -160,10 +156,15 @@ impl TextDocument { // private methods impl TextDocument { fn parse_typed_tokens_from_text(&self) -> Option> { - let text = Arc::from(self.get_text()); - let namespace = namespace::Module::default(); - let ast_res = sway_core::compile_to_ast(text, namespace, None); - match ast_res { + let manifest_dir = PathBuf::from(self.get_uri()); + let silent_mode = true; + let manifest = + pkg::ManifestFile::from_dir(&manifest_dir, forc::utils::SWAY_GIT_TAG).unwrap(); + let lock_path = forc_util::lock_path(manifest.dir()); + let plan = pkg::BuildPlan::from_lock_file(&lock_path, forc::utils::SWAY_GIT_TAG).unwrap(); + let res = pkg::check(&plan, silent_mode, forc::utils::SWAY_GIT_TAG).unwrap(); + + match res { CompileAstResult::Failure { .. } => None, CompileAstResult::Success { typed_program, .. } => Some(typed_program.root.all_nodes), } diff --git a/sway-lsp/src/server.rs b/sway-lsp/src/server.rs index f167fa4af4f..5a66c1d9182 100644 --- a/sway-lsp/src/server.rs +++ b/sway-lsp/src/server.rs @@ -242,7 +242,7 @@ impl LanguageServer for Backend { #[cfg(test)] mod tests { use serde_json::json; - use std::{env, fs::File, io::Write}; + use std::{env, fs, io::Read}; use tower::{Service, ServiceExt}; use super::*; @@ -250,46 +250,20 @@ mod tests { use tower_lsp::jsonrpc::{self, Request, Response}; use tower_lsp::LspService; - // Simple sway script used for testing LSP capabilites - const SWAY_PROGRAM: &str = r#"script; - -//use std::*; - -/// A simple Particle struct -struct Particle { - position: [u64; 3], - velocity: [u64; 3], - acceleration: [u64; 3], - mass: u64, -} - -impl Particle { - /// Creates a new Particle with the given position, velocity, acceleration, and mass - fn new(position: [u64; 3], velocity: [u64; 3], acceleration: [u64; 3], mass: u64) -> Particle { - Particle { - position: position, - velocity: velocity, - acceleration: acceleration, - mass: mass, - } - } -} + fn load_sway_example() -> (Url, String) { + let manifest_dir = env::current_dir() + .unwrap() + .parent() + .unwrap() + .join("examples/liquidity_pool"); + let src_path = manifest_dir.join("src/main.sw"); + let mut file = fs::File::open(&src_path).unwrap(); + let mut sway_program = String::new(); + file.read_to_string(&mut sway_program).unwrap(); -fn main() { - let position = [0, 0, 0]; - let velocity = [0, 1, 0]; - let acceleration = [1, 1, 0]; - let mass = 10; - let p = ~Particle::new(position, velocity, acceleration, mass); -} -"#; + let uri = Url::from_file_path(src_path).unwrap(); - fn load_test_sway_file(sway_file: &str) -> Url { - let file_name = "tmp_sway_test_file.sw"; - let dir = env::temp_dir().join(file_name); - let mut file = File::create(&dir).unwrap(); - file.write_all(sway_file.as_bytes()).unwrap(); - Url::from_file_path(dir.as_os_str().to_str().unwrap()).unwrap() + (uri, sway_program) } async fn initialize_request(service: &mut LspService) -> Request { @@ -440,10 +414,10 @@ fn main() { // ignore the "window/logMessage" notification: "Initializing the Sway Language Server" messages.next().await.unwrap(); - let uri = load_test_sway_file(SWAY_PROGRAM); + let (uri, sway_program) = load_sway_example(); // send "textDocument/didOpen" notification for `uri` - did_open_notification(&mut service, &uri, SWAY_PROGRAM).await; + did_open_notification(&mut service, &uri, &sway_program).await; // ignore the "textDocument/publishDiagnostics" notification messages.next().await.unwrap(); @@ -465,10 +439,10 @@ fn main() { // send "initialized" notification initialized_notification(&mut service).await; - let uri = load_test_sway_file(SWAY_PROGRAM); + let (uri, sway_program) = load_sway_example(); // send "textDocument/didOpen" notification for `uri` - did_open_notification(&mut service, &uri, SWAY_PROGRAM).await; + did_open_notification(&mut service, &uri, &sway_program).await; // send "textDocument/didClose" notification for `uri` did_close_notification(&mut service).await;