Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions ls/src/document.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
use std::fs;
use std::io::Error;

use async_lsp::lsp_types::Url;

use yara_x_parser::cst::CST;
Expand All @@ -23,4 +26,11 @@ impl Document {
let line_index = LineIndex::new(text.as_str());
Self { uri, text, cst, line_index }
}

/// Reads a document from a uri.
pub fn read(uri: Url) -> Result<Self, Error> {
let path =
uri.to_file_path().map_err(|_| Error::other("invalid path"))?;
Ok(Self::new(uri, fs::read_to_string(path)?))
}
}
72 changes: 65 additions & 7 deletions ls/src/features/goto.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use async_lsp::lsp_types::{Position, Range};
use std::path::PathBuf;

use async_lsp::lsp_types::{Location, Position, Url};
use yara_x_parser::cst::SyntaxKind;

use crate::document::Document;
Expand All @@ -10,7 +12,10 @@ use crate::utils::position::node_to_range;

/// Given a position that points some identifier, returns the range
/// of source code that contains the definition of that identifier.
pub fn go_to_definition(document: &Document, pos: Position) -> Option<Range> {
pub fn go_to_definition(
document: &Document,
pos: Position,
) -> Option<Location> {
let token = ident_at_position(&document.cst, pos)?;

#[allow(irrefutable_let_patterns)]
Expand All @@ -23,13 +28,66 @@ pub fn go_to_definition(document: &Document, pos: Position) -> Option<Range> {
| SyntaxKind::PATTERN_LENGTH => {
let rule = rule_containing_token(&token)?;
let pattern = pattern_from_ident(&rule, token.text())?;
node_to_range(&pattern)
let range = node_to_range(&pattern)?;
Some(Location { uri: document.uri.clone(), range })
}
// Rule identifiers
SyntaxKind::IDENT => {
let rule = rule_from_ident(&document.cst, token.text())?;
node_to_range(&rule)
}
SyntaxKind::IDENT => go_to_rule_definition(document, token.text()),
_ => None,
}
}

fn go_to_rule_definition(
document: &Document,
ident: &str,
) -> Option<Location> {
// Check if the rule is defined in the current document
if let Some(rule) = rule_from_ident(&document.cst, ident) {
return Some(Location {
uri: document.uri.clone(),
range: node_to_range(&rule)?,
});
}

// If the rule is not declared in the current document, we need to look
// into any included document.
let included_paths = document
.cst
.root()
.children()
.filter(|node| node.kind() == SyntaxKind::INCLUDE_STMT)
.filter_map(|include_stmt| {
include_stmt
.children_with_tokens()
.find(|t| t.kind() == SyntaxKind::STRING_LIT)
.and_then(|n| n.into_token())
});

let document_path = document.uri.to_file_path().unwrap();
let document_dir = document_path.parent().unwrap();

for included_path in included_paths {
let included_path = included_path.text();

// Remove the quotes surrounding the included path.
let included_path =
PathBuf::from(&included_path[1..included_path.len() - 1]);

// If the included path is already absolute, use it as is,
// join it to the current document directory if otherwise.
let abs_included_path = if included_path.is_absolute() {
included_path
} else {
document_dir.join(included_path)
};

let uri = Url::from_file_path(abs_included_path).ok()?;
let document = Document::read(uri).ok()?;

if let Some(location) = go_to_rule_definition(&document, ident) {
return Some(location);
}
}

None
}
5 changes: 2 additions & 3 deletions ls/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,9 +224,8 @@ impl LanguageServer for YARALanguageServer {
None => return Box::pin(async { Ok(None) }),
};

let definition = go_to_definition(document, position).map(|range| {
GotoDefinitionResponse::Scalar(Location { uri, range })
});
let definition = go_to_definition(document, position)
.map(GotoDefinitionResponse::Scalar);

Box::pin(async move { Ok(definition) })
}
Expand Down
86 changes: 71 additions & 15 deletions ls/src/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
use std::fmt::Debug;
use std::fs;
use std::fs::File;
use std::future::Future;
use std::path::{Path, PathBuf};

Expand All @@ -19,12 +18,13 @@ use async_lsp::lsp_types::{
ClientCapabilities, DiagnosticClientCapabilities,
DidCloseTextDocumentParams, DidOpenTextDocumentParams, InitializeParams,
InitializedParams, TextDocumentClientCapabilities, TextDocumentIdentifier,
TextDocumentItem, Url,
TextDocumentItem, Url, WorkspaceFolder,
};
use async_lsp::router::Router;
use async_lsp::server::LifecycleLayer;
use async_lsp::{LanguageServer, ServerSocket};
use futures::AsyncReadExt;
use serde_json::Value;
use tokio_util::compat::TokioAsyncReadCompatExt;
use tower::ServiceBuilder;

Expand Down Expand Up @@ -59,9 +59,18 @@ where
_ = server.run_buffered(server_rx, server_tx) => {}
_ = client.run_buffered(client_rx, client_tx) => {}
_ = async {
let root_path = PathBuf::from("src/tests/testdata");
let root_uri = Url::from_file_path(
root_path.canonicalize().unwrap()
).unwrap();

// Send request to initialize the server.
server_socket
.initialize(InitializeParams{
workspace_folders: Some(vec![WorkspaceFolder{
uri: root_uri,
name: "testdata".to_string(),
}]),
capabilities: ClientCapabilities {
text_document: Some(TextDocumentClientCapabilities {
diagnostic: Some(DiagnosticClientCapabilities {
Expand Down Expand Up @@ -90,13 +99,12 @@ where

async fn open_document<P: AsRef<Path>>(s: &ServerSocket, path: P) {
let path = path.as_ref();
let filename = path.file_name().unwrap().to_str().unwrap();
let rule = fs::read_to_string(path)
.unwrap_or_else(|_| panic!("failed to read file {path:?}"));

s.notify::<DidOpenTextDocument>(DidOpenTextDocumentParams {
text_document: TextDocumentItem {
uri: Url::parse(format!("file:///{filename}").as_str()).unwrap(),
uri: Url::from_file_path(path).unwrap(),
language_id: "yara".to_string(),
version: 1,
text: rule,
Expand All @@ -107,11 +115,10 @@ async fn open_document<P: AsRef<Path>>(s: &ServerSocket, path: P) {

async fn close_document<P: AsRef<Path>>(s: &ServerSocket, path: P) {
let path = path.as_ref();
let filename = path.file_name().unwrap().to_str().unwrap();

s.notify::<DidCloseTextDocument>(DidCloseTextDocumentParams {
text_document: TextDocumentIdentifier {
uri: Url::parse(format!("file:///{filename}").as_str()).unwrap(),
uri: Url::from_file_path(path).unwrap(),
},
})
.expect("DidOpenTextDocument notification failed");
Expand All @@ -122,35 +129,83 @@ where
R::Result: serde::Serialize + serde::de::DeserializeOwned + Debug,
{
let path = PathBuf::from("src/tests/testdata").join(path);
let abs_path = path.canonicalize().unwrap();
let test_dir = Url::from_file_path(abs_path.parent().unwrap()).unwrap();

lsp_test(async |server_socket| {
open_document(&server_socket, path.as_path()).await;
open_document(&server_socket, &abs_path).await;

let mut mint = goldenfile::Mint::new(".");

let request_path = path.with_extension("request.json");
let request_file = File::open(request_path.as_path())
.unwrap_or_else(|_| panic!("can't read {request_path:?}"));
let request_str = fs::read_to_string(request_path.as_path())
.unwrap_or_else(|_| panic!("can't read {request_path:?}"))
.replace("${test_dir}", test_dir.as_str());

let request = serde_json::from_str::<R::Params>(&request_str)
.unwrap_or_else(|_| {
panic!("failed to parse request: {}", request_str)
});

let response_path = path.with_extension("response.json");
let response_file = mint
.new_goldenfile(response_path.as_path())
.unwrap_or_else(|_| panic!("can't read {request_path:?}"));

let request =
serde_json::from_reader::<_, R::Params>(request_file).unwrap();
let actual_response = match server_socket.request::<R>(request).await {
Ok(response) => response,
Err(err) => {
panic!("request failed: {:?}", err)
}
};

let actual_response =
server_socket.request::<R>(request).await.unwrap();
close_document(&server_socket, &abs_path).await;

close_document(&server_socket, path.as_path()).await;
let mut response_json = serde_json::to_value(actual_response).unwrap();

serde_json::to_writer_pretty(response_file, &actual_response).unwrap();
replace_in_json(&mut response_json, test_dir.as_str(), "${test_dir}");
serde_json::to_writer_pretty(response_file, &response_json).unwrap();
server_socket
})
.await;
}

/// Replaces all occurrences of `from` with `to` in a JSON value.
///
/// This function recursively traverses the JSON value and performs the
/// replacement in string values and in object keys.
fn replace_in_json(value: &mut Value, from: &str, to: &str) {
match value {
Value::Object(map) => {
let keys_to_modify: Vec<(String, String)> = map
.keys()
.filter(|k| k.contains(from))
.map(|k| (k.clone(), k.replace(from, to)))
.collect();

for (old_key, new_key) in keys_to_modify {
if let Some(mut val) = map.remove(&old_key) {
replace_in_json(&mut val, from, to);
map.insert(new_key, val);
}
}

for (_, val) in map.iter_mut() {
replace_in_json(val, from, to);
}
}
Value::Array(arr) => {
for v in arr {
replace_in_json(v, from, to);
}
}
Value::String(s) => {
*s = s.replace(from, to);
}
_ => {}
}
}

#[tokio::test]
async fn selection_range() {
test_lsp_request::<_, SelectionRangeRequest>("selectionrange1.yar").await;
Expand Down Expand Up @@ -187,6 +242,7 @@ async fn goto_definition() {
test_lsp_request::<_, GotoDefinition>("goto3.yar").await;
test_lsp_request::<_, GotoDefinition>("goto4.yar").await;
test_lsp_request::<_, GotoDefinition>("goto5.yar").await;
test_lsp_request::<_, GotoDefinition>("goto6.yar").await;
}

#[tokio::test]
Expand Down
2 changes: 1 addition & 1 deletion ls/src/tests/testdata/code_action.request.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"textDocument": {
"uri": "file:///code_action.yar"
"uri": "${test_dir}/code_action.yar"
},
"range": {
"start": {
Expand Down
2 changes: 1 addition & 1 deletion ls/src/tests/testdata/code_action.response.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"kind": "quickfix",
"edit": {
"changes": {
"file:///code_action.yar": [
"${test_dir}/code_action.yar": [
{
"range": {
"start": {
Expand Down
2 changes: 1 addition & 1 deletion ls/src/tests/testdata/completion1.request.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"textDocument": {
"uri": "file:///completion1.yar"
"uri": "${test_dir}/completion1.yar"
},
"position": {
"line": 0,
Expand Down
2 changes: 1 addition & 1 deletion ls/src/tests/testdata/completion10.request.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"textDocument": {
"uri": "file:///completion10.yar"
"uri": "${test_dir}/completion10.yar"
},
"position": {
"line": 0,
Expand Down
2 changes: 1 addition & 1 deletion ls/src/tests/testdata/completion11.request.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"textDocument": {
"uri": "file:///completion11.yar"
"uri": "${test_dir}/completion11.yar"
},
"position": {
"line": 0,
Expand Down
2 changes: 1 addition & 1 deletion ls/src/tests/testdata/completion12.request.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"textDocument": {
"uri": "file:///completion12.yar"
"uri": "${test_dir}/completion12.yar"
},
"position": {
"line": 2,
Expand Down
2 changes: 1 addition & 1 deletion ls/src/tests/testdata/completion13.request.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"textDocument": {
"uri": "file:///completion13.yar"
"uri": "${test_dir}/completion13.yar"
},
"position": {
"line": 3,
Expand Down
2 changes: 1 addition & 1 deletion ls/src/tests/testdata/completion2.request.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"textDocument": {
"uri": "file:///completion2.yar"
"uri": "${test_dir}/completion2.yar"
},
"position": {
"line": 2,
Expand Down
2 changes: 1 addition & 1 deletion ls/src/tests/testdata/completion3.request.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"textDocument": {
"uri": "file:///completion3.yar"
"uri": "${test_dir}/completion3.yar"
},
"position": {
"line": 4,
Expand Down
2 changes: 1 addition & 1 deletion ls/src/tests/testdata/completion4.request.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"textDocument": {
"uri": "file:///completion4.yar"
"uri": "${test_dir}/completion4.yar"
},
"position": {
"line": 2,
Expand Down
2 changes: 1 addition & 1 deletion ls/src/tests/testdata/completion5.request.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"textDocument": {
"uri": "file:///completion5.yar"
"uri": "${test_dir}/completion5.yar"
},
"position": {
"line": 2,
Expand Down
2 changes: 1 addition & 1 deletion ls/src/tests/testdata/completion6.request.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"textDocument": {
"uri": "file:///completion6.yar"
"uri": "${test_dir}/completion6.yar"
},
"position": {
"line": 1,
Expand Down
2 changes: 1 addition & 1 deletion ls/src/tests/testdata/completion7.request.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"textDocument": {
"uri": "file:///completion7.yar"
"uri": "${test_dir}/completion7.yar"
},
"position": {
"line": 8,
Expand Down
Loading
Loading