Skip to content

Commit

Permalink
LS: Add a command to inspect analyzed crates
Browse files Browse the repository at this point in the history
commit-id:26737288
  • Loading branch information
mkaput committed Jul 10, 2024
1 parent 600e495 commit a351465
Show file tree
Hide file tree
Showing 16 changed files with 289 additions and 6 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion crates/cairo-lang-language-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ cairo-lang-starknet = { path = "../cairo-lang-starknet", version = "~2.6.4" }
cairo-lang-syntax = { path = "../cairo-lang-syntax", version = "~2.6.4" }
cairo-lang-test-plugin = { path = "../cairo-lang-test-plugin", version = "~2.6.4" }
cairo-lang-utils = { path = "../cairo-lang-utils", version = "~2.6.4" }
indent.workspace = true
indoc.workspace = true
itertools.workspace = true
salsa.workspace = true
scarb-metadata = "1.12"
Expand All @@ -42,7 +44,6 @@ cairo-lang-language-server = { path = ".", features = ["testing"] }
assert_fs = "1.1"
cairo-lang-test-utils = { path = "../cairo-lang-test-utils", features = ["testing"] }
futures = "0.3"
indoc.workspace = true
pathdiff = "0.2"
test-log.workspace = true
tower-service = "0.3"
40 changes: 40 additions & 0 deletions crates/cairo-lang-language-server/src/lang/inspect/crates.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use cairo_lang_filesystem::db::FilesGroup;
use indent::indent_by;
use indoc::formatdoc;
use itertools::Itertools;

use crate::lang::db::AnalysisDatabase;
use crate::project::Crate;

/// Generates a Markdown text describing all crates in the database.
pub fn inspect_analyzed_crates(db: &AnalysisDatabase) -> String {
let list = db
.crates()
.into_iter()
.flat_map(|crate_id| Crate::reconstruct(db, crate_id))
.sorted_by_key(|cr| cr.name.clone())
.map(inspect_crate)
.collect::<Vec<_>>()
.join("");
formatdoc! {r#"
# Analyzed Crates
{list}
"#}
}

/// Generates a Markdown fragment describing a single crate.
fn inspect_crate(cr: Crate) -> String {
formatdoc! {
r#"
- `{name}`: `{source_path}`
```rust
{settings}
```
"#,
name = cr.name,
source_path = cr.source_path().display(),
settings = indent_by(4, format!("{:#?}", cr.settings)),
}
}
3 changes: 2 additions & 1 deletion crates/cairo-lang-language-server/src/lang/inspect/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
//! High-level constructs for inspecting language elements from the Salsa database.
//! High-level constructs for inspecting language elements from the analysis database.

pub mod crates;
pub mod defs;
9 changes: 8 additions & 1 deletion crates/cairo-lang-language-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ use salsa::ParallelDatabase;
use serde_json::Value;
use tokio::task::spawn_blocking;
use tower_lsp::jsonrpc::{Error as LSPError, Result as LSPResult};
use tower_lsp::lsp_types::request::Request;
use tower_lsp::lsp_types::*;
use tower_lsp::{Client, ClientSocket, LanguageServer, LspService, Server};
use tracing::{debug, error, info, trace_span, warn, Instrument};
Expand All @@ -98,7 +99,7 @@ mod config;
mod env_config;
mod ide;
mod lang;
mod lsp;
pub mod lsp;
mod markdown;
mod project;
mod server;
Expand Down Expand Up @@ -272,6 +273,7 @@ impl Backend {
fn build_service(tricks: Tricks) -> (LspService<Self>, ClientSocket) {
LspService::build(|client| Self::new(client, tricks))
.custom_method("vfs/provide", Self::vfs_provide)
.custom_method(lsp::ext::ViewAnalyzedCrates::METHOD, Self::view_analyzed_crates)
.finish()
}

Expand Down Expand Up @@ -508,6 +510,11 @@ impl Backend {
}
}

#[tracing::instrument(level = "trace", skip_all)]
async fn view_analyzed_crates(&self) -> LSPResult<String> {
self.with_db(lang::inspect::crates::inspect_analyzed_crates).await
}

#[tracing::instrument(level = "trace", skip_all)]
async fn vfs_provide(
&self,
Expand Down
13 changes: 13 additions & 0 deletions crates/cairo-lang-language-server/src/lsp/ext.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//! CairoLS extensions to the Language Server Protocol.

use tower_lsp::lsp_types::request::Request;

// TODO(mkaput): Provide this as a command in VSCode.
/// Collect information about all Cairo crates that are currently being analyzed.
pub struct ViewAnalyzedCrates;

impl Request for ViewAnalyzedCrates {
type Params = ();
type Result = String;
const METHOD: &'static str = "cairo/viewAnalyzedCrates";
}
3 changes: 2 additions & 1 deletion crates/cairo-lang-language-server/src/lsp/mod.rs
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
pub mod client_capabilities;
pub(crate) mod client_capabilities;
pub mod ext;
51 changes: 49 additions & 2 deletions crates/cairo-lang-language-server/src/project/crate_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ use std::sync::Arc;
use cairo_lang_defs::db::DefsGroup;
use cairo_lang_defs::ids::ModuleId;
use cairo_lang_filesystem::db::{
AsFilesGroupMut, CrateConfiguration, CrateSettings, FilesGroupEx, CORELIB_CRATE_NAME,
AsFilesGroupMut, CrateConfiguration, CrateSettings, FilesGroup, FilesGroupEx,
CORELIB_CRATE_NAME,
};
use cairo_lang_filesystem::ids::{CrateId, CrateLongId, Directory};
use cairo_lang_utils::Intern;
use cairo_lang_utils::{Intern, LookupIntern};
use smol_str::SmolStr;

use crate::lang::db::AnalysisDatabase;
Expand Down Expand Up @@ -49,10 +50,37 @@ impl Crate {
}
}

/// Construct a [`Crate`] from data already applied to the [`AnalysisDatabase`].
///
/// Returns `None` if the crate is virtual or the crate configuration is missing.
pub fn reconstruct(db: &AnalysisDatabase, crate_id: CrateId) -> Option<Self> {
let CrateLongId::Real(name) = crate_id.lookup_intern(db) else {
return None;
};

let Some(CrateConfiguration { root: Directory::Real(root), settings }) =
db.crate_config(crate_id)
else {
return None;
};

let custom_main_file_stem = extract_custom_file_stem(db, crate_id);

Some(Self { name, root, custom_main_file_stem, settings })
}

/// States whether this is the `core` crate.
pub fn is_core(&self) -> bool {
self.name == CORELIB_CRATE_NAME
}

/// Returns the path to the main file of this crate.
pub fn source_path(&self) -> PathBuf {
self.root.join(match &self.custom_main_file_stem {
Some(stem) => format!("{stem}.cairo"),
None => "lib.cairo".into(),
})
}
}

/// Generate a wrapper lib file for a compilation unit without a root `lib.cairo`.
Expand All @@ -67,3 +95,22 @@ fn inject_virtual_wrapper_lib(db: &mut AnalysisDatabase, crate_id: CrateId, file
db.as_files_group_mut()
.override_file_content(file_id, Some(Arc::new(format!("mod {file_stem};"))));
}

/// The inverse of [`inject_virtual_wrapper_lib`],
/// tries to infer root module name from crate if it does not have real `lib.cairo`.
fn extract_custom_file_stem(db: &AnalysisDatabase, crate_id: CrateId) -> Option<SmolStr> {
let CrateConfiguration { root: Directory::Real(root), .. } = db.crate_config(crate_id)? else {
return None;
};

if root.join("lib.cairo").exists() {
return None;
}

let module_id = ModuleId::CrateRoot(crate_id);
let file_id = db.module_main_file(module_id).ok()?;
let content = db.file_content(file_id)?;

let name = content.strip_prefix("mod ")?.strip_suffix(';')?;
Some(name.into())
}
1 change: 1 addition & 0 deletions crates/cairo-lang-language-server/src/project/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub use self::crate_data::Crate;
pub use self::project_manifest_path::*;

mod crate_data;
Expand Down
38 changes: 38 additions & 0 deletions crates/cairo-lang-language-server/tests/e2e/analysis.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
use cairo_lang_language_server::lsp;
use cairo_lang_test_utils::parse_test_file::TestRunnerResult;
use cairo_lang_utils::ordered_hash_map::OrderedHashMap;

use crate::support::normalize::normalize;
use crate::support::sandbox;

cairo_lang_test_utils::test_file_test!(
project,
"tests/test_data/analysis/crates",
{
cairo_projects: "cairo_projects.txt",
},
test_analyzed_crates
);

fn test_analyzed_crates(
inputs: &OrderedHashMap<String, String>,
_args: &OrderedHashMap<String, String>,
) -> TestRunnerResult {
let dyn_files = inputs.iter().flat_map(|(p, c)| Some((p.strip_prefix("file: ")?, c)));

let mut ls = sandbox! {
dyn_files(dyn_files)
};

for path_to_open in inputs["open files in order"].lines() {
ls.open_and_wait_for_diagnostics(path_to_open);
}

let output = ls.send_request::<lsp::ext::ViewAnalyzedCrates>(());
let output = normalize(&ls, output);

TestRunnerResult::success(OrderedHashMap::from([(
"expected analyzed crates".to_owned(),
output,
)]))
}
1 change: 1 addition & 0 deletions crates/cairo-lang-language-server/tests/e2e/main.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod analysis;
mod hover;
mod semantic_tokens;
mod support;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ impl Fixture {

/// Introspection methods.
impl Fixture {
pub fn root_path(&self) -> &Path {
self.t.path()
}

pub fn root_url(&self) -> Url {
Url::from_directory_path(self.t.path()).unwrap()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -418,3 +418,9 @@ impl MockClient {
.collect()
}
}

impl AsRef<Fixture> for MockClient {
fn as_ref(&self) -> &Fixture {
&self.fixture
}
}
8 changes: 8 additions & 0 deletions crates/cairo-lang-language-server/tests/e2e/support/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub mod cursor;
pub mod fixture;
pub mod jsonrpc;
mod mock_client;
pub mod normalize;
mod runtime;

pub use self::cursor::cursors;
Expand All @@ -17,6 +18,7 @@ pub use self::mock_client::MockClient;
macro_rules! sandbox {
(
$(files { $($file:expr => $content:expr),* $(,)? })?
$(dyn_files($dyn_file_iter:expr))?
$(client_capabilities = $client_capabilities:expr;)?
$(workspace_configuration = $workspace_configuration:expr;)?
) => {{
Expand All @@ -30,6 +32,12 @@ macro_rules! sandbox {

$($(fixture.add_file($file, $content);)*)?

$(
for (file, content) in $dyn_file_iter {
fixture.add_file(file, content);
}
)?

#[allow(unused_mut)]
let mut client_capabilities = client_capabilities::base();

Expand Down
36 changes: 36 additions & 0 deletions crates/cairo-lang-language-server/tests/e2e/support/normalize.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use std::path::Path;

use crate::support::fixture::Fixture;

/// Performs various normalization steps of the input data, to remove any runtime-specific artifacts
/// and make comparisons in test assertions deterministic.
pub fn normalize(fixture: impl AsRef<Fixture>, data: impl ToString) -> String {
let fixture = fixture.as_ref();
normalize_well_known_paths(fixture, normalize_paths(data.to_string()))
}

/// Replace all well-known paths/urls for a fixture with placeholders.
fn normalize_well_known_paths(fixture: &Fixture, data: String) -> String {
let mut data = data
.replace(&fixture.root_url().to_string(), "[ROOT_URL]")
.replace(&normalize_path(fixture.root_path()), "[ROOT]");

if let Ok(pwd) = std::env::current_dir() {
data = data.replace(&normalize_path(&pwd), "[PWD]");
}

let cairo_source = Path::new(env!("CARGO_MANIFEST_DIR")).parent().unwrap().parent().unwrap();
data = data.replace(&normalize_path(cairo_source), "[CAIRO_SOURCE]");

data
}

/// Normalizes path separators.
fn normalize_paths(data: String) -> String {
data.replace('\\', "/")
}

/// Normalize a path to a consistent format.
fn normalize_path(path: &Path) -> String {
normalize_paths(path.to_string_lossy().to_string())
}
Loading

0 comments on commit a351465

Please sign in to comment.