diff --git a/.github/workflows/publish_npm_package.yaml b/.github/workflows/publish_npm_package.yaml index 875efc20c..fdb59efbb 100644 --- a/.github/workflows/publish_npm_package.yaml +++ b/.github/workflows/publish_npm_package.yaml @@ -27,6 +27,9 @@ jobs: node-version: 24 registry-url: https://registry.npmjs.org/ + - name: Install toml + run: pip install toml + - name: Install wasm-pack run: cargo install --locked wasm-pack diff --git a/Cargo.lock b/Cargo.lock index 16d670fed..08497cae6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4871,7 +4871,7 @@ dependencies = [ [[package]] name = "yara-x" -version = "1.14.0" +version = "1.15.0" dependencies = [ "aho-corasick", "annotate-snippets", @@ -4946,7 +4946,7 @@ dependencies = [ [[package]] name = "yara-x-capi" -version = "1.14.0" +version = "1.15.0" dependencies = [ "assert-call", "cbindgen", @@ -4956,7 +4956,7 @@ dependencies = [ [[package]] name = "yara-x-cli" -version = "1.14.0" +version = "1.15.0" dependencies = [ "anyhow", "ascii_tree", @@ -4996,7 +4996,7 @@ dependencies = [ [[package]] name = "yara-x-fmt" -version = "1.14.0" +version = "1.15.0" dependencies = [ "bitflags 2.11.0", "bstr", @@ -5010,7 +5010,7 @@ dependencies = [ [[package]] name = "yara-x-js" -version = "1.14.0" +version = "1.15.0" dependencies = [ "getrandom 0.2.17", "js-sys", @@ -5026,7 +5026,7 @@ dependencies = [ [[package]] name = "yara-x-ls" -version = "1.14.0" +version = "1.15.0" dependencies = [ "async-lsp", "bitflags 2.11.0", @@ -5056,7 +5056,7 @@ dependencies = [ [[package]] name = "yara-x-macros" -version = "1.14.0" +version = "1.15.0" dependencies = [ "darling", "proc-macro2", @@ -5066,7 +5066,7 @@ dependencies = [ [[package]] name = "yara-x-parser" -version = "1.14.0" +version = "1.15.0" dependencies = [ "anyhow", "ascii_tree", @@ -5089,7 +5089,7 @@ dependencies = [ [[package]] name = "yara-x-proto" -version = "1.14.0" +version = "1.15.0" dependencies = [ "protobuf", "protobuf-codegen", @@ -5097,7 +5097,7 @@ dependencies = [ [[package]] name = "yara-x-proto-json" -version = "1.14.0" +version = "1.15.0" dependencies = [ "base64 0.22.1", "globwalk", @@ -5111,7 +5111,7 @@ dependencies = [ [[package]] name = "yara-x-proto-yaml" -version = "1.14.0" +version = "1.15.0" dependencies = [ "chrono", "globwalk", @@ -5125,7 +5125,7 @@ dependencies = [ [[package]] name = "yara-x-py" -version = "1.14.0" +version = "1.15.0" dependencies = [ "base64 0.22.1", "protobuf", diff --git a/Cargo.toml b/Cargo.toml index e72afa68b..7346f0035 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace.package] -version = "1.14.0" +version = "1.15.0" authors = ["Victor M. Alvarez "] edition = "2024" homepage = "https://virustotal.github.io/yara-x" @@ -110,13 +110,13 @@ wasm-opt = "0.116.1" wasmtime = { version = "40.0.4", default-features = false } x509-parser = "0.18.0" yansi = "1.0.1" -yara-x = { path = "lib", version = "1.14.0" } -yara-x-fmt = { path = "fmt", version = "1.14.0" } -yara-x-macros = { path = "macros", version = "1.14.0" } -yara-x-parser = { path = "parser", version = "1.14.0" } -yara-x-proto = { path = "proto", version = "1.14.0"} -yara-x-proto-yaml = { path = "proto-yaml", version = "1.14.0" } -yara-x-proto-json = { path = "proto-json", version = "1.14.0" } +yara-x = { path = "lib", version = "1.15.0" } +yara-x-fmt = { path = "fmt", version = "1.15.0" } +yara-x-macros = { path = "macros", version = "1.15.0" } +yara-x-parser = { path = "parser", version = "1.15.0" } +yara-x-proto = { path = "proto", version = "1.15.0"} +yara-x-proto-yaml = { path = "proto-yaml", version = "1.15.0" } +yara-x-proto-json = { path = "proto-json", version = "1.15.0" } zip = { version = "8.2.0", default-features = false } simd-adler32 = "0.3.8" simd_cesu8 = "1.1.1" diff --git a/lib/src/modules/mod.rs b/lib/src/modules/mod.rs index 5e7b276a9..bec1cc96d 100644 --- a/lib/src/modules/mod.rs +++ b/lib/src/modules/mod.rs @@ -403,11 +403,30 @@ pub mod mods { #[derive(Clone, Debug, PartialEq)] pub struct FuncSignature { /// The names and types of the function arguments. - pub args: Vec<(String, Type)>, + args: Vec<(String, Type)>, /// The return type for the function. - pub ret: Type, + ret: Type, /// Function's documentation. - pub doc: Option>, + doc: Option>, + } + + impl FuncSignature { + /// The names and types of the function arguments. + pub fn args( + &self, + ) -> impl ExactSizeIterator { + self.args.iter().map(|(name, ty)| (name.as_str(), ty)) + } + + /// The return type for the function. + pub fn ret_type(&self) -> &Type { + &self.ret + } + + /// Function's documentation. + pub fn doc(&self) -> Option<&str> { + self.doc.as_deref() + } } /// Describes a field within a structure or module. diff --git a/ls/src/features/completion.rs b/ls/src/features/completion.rs index 867107a89..120e991b2 100644 --- a/ls/src/features/completion.rs +++ b/ls/src/features/completion.rs @@ -18,7 +18,7 @@ use crate::utils::cst_traversal::{ rule_containing_token, token_at_position, }; -use crate::utils::modules::{get_struct, ty_to_string}; +use crate::utils::modules::{get_type, ty_to_string}; const PATTERN_MODS: &[(SyntaxKind, &[&str])] = &[ ( @@ -380,7 +380,7 @@ fn field_suggestions(token: &Token) -> Option> { _ => None, }?; - let current_struct = match get_struct(&token)? { + let current_struct = match get_type(&token)? { Type::Struct(s) => s, _ => return None, }; @@ -399,14 +399,12 @@ fn field_suggestions(token: &Token) -> Option> { .iter() .map(|sig| { let args = sig - .args - .iter() + .args() .map(|(name, ty)| format!("{}: {}", name, ty_to_string(ty))) .collect::>(); let args_template = sig - .args - .iter() + .args() .enumerate() .map(|(n, (name, _))| { format!("${{{}:{name}}}", n + 1) @@ -430,7 +428,7 @@ fn field_suggestions(token: &Token) -> Option> { description: Some(ty_to_string(&ty)), ..Default::default() }), - documentation: sig.doc.as_ref().map( + documentation: sig.doc().map( |docs| { async_lsp::lsp_types::Documentation::MarkupContent( async_lsp::lsp_types::MarkupContent { @@ -438,11 +436,10 @@ fn field_suggestions(token: &Token) -> Option> { value: format!( "## `{}({}) -> {}`\n\n{}", name, - sig.args - .iter() + sig.args() .map(|(name, ty)| format!("{}: {}", name, ty_to_string(ty))) .join(", "), - ty_to_string(&sig.ret), + ty_to_string(sig.ret_type()), docs ), }, diff --git a/ls/src/features/hover.rs b/ls/src/features/hover.rs index dbe444774..f2726af0f 100644 --- a/ls/src/features/hover.rs +++ b/ls/src/features/hover.rs @@ -4,16 +4,16 @@ use async_lsp::lsp_types::{ HoverContents, MarkupContent, MarkupKind, Position, Url, }; use itertools::Itertools; - +use yara_x::mods::reflect::Type; use yara_x_parser::cst::{Immutable, Node, NodeOrToken, SyntaxKind, Utf8}; use crate::documents::storage::DocumentStorage; use crate::utils::cst_traversal::{ - find_declaration, pattern_from_ident, rule_containing_token, - token_at_position, + find_declaration, pattern_from_ident, prev_non_trivia_token, + rule_containing_token, token_at_position, }; -use crate::utils::modules::{get_struct, ty_to_string}; +use crate::utils::modules::{get_type, ty_to_string}; /// Builder for hover Markdown representation of a rule. struct RuleHoverBuilder { @@ -106,38 +106,71 @@ pub fn hover( } // Other identifiers. SyntaxKind::IDENT => { - if let Some(yara_x::mods::reflect::Type::Func(func)) = - get_struct(&token) - { - let documentation = func - .signatures - .iter() - .filter_map(|signature| { - signature.doc.as_ref().map(|doc| { - format!( - "### `{}({}) -> {}`\n\n***\n\n{}\n\n***\n\n", - token.text(), - signature - .args - .iter() - .map(|(name, ty)| format!( - "{}: {}", - name, - ty_to_string(ty) - )) - .join(", "), - ty_to_string(&signature.ret), - doc - ) - }) - }) - .join("\n"); - - return Some(HoverContents::Markup(MarkupContent { - kind: MarkupKind::Markdown, - value: documentation, - })); + let structure = prev_non_trivia_token(&token) + .filter(|token| token.kind() == SyntaxKind::DOT) + .and_then(|token| prev_non_trivia_token(&token)) + .and_then(|token| get_type(&token)) + .and_then(|ty| { + if let Type::Struct(s) = ty { Some(s) } else { None } + }); + + let field = structure + .as_ref() + .and_then(|s| s.fields().find(|f| f.name() == token.text())); + + if let Some(field) = field { + match field.ty() { + Type::Func(func) => { + let documentation = func + .signatures + .iter() + .filter_map(|signature| { + signature.doc().map(|doc| { + format!( + "### `{}({}) -> {}`\n\n***\n\n{}\n\n***\n\n", + token.text(), + signature + .args() + .map(|(arg_name, arg_ty)| format!( + "{}: {}", + arg_name, + ty_to_string(arg_ty) + )) + .join(", "), + ty_to_string(signature.ret_type()), + doc + ) + }) + }) + .join("\n"); + + if !documentation.is_empty() { + return Some(HoverContents::Markup( + MarkupContent { + kind: MarkupKind::Markdown, + value: documentation, + }, + )); + } + } + ty => { + let mut value = format!( + "### `{}: {}`", + token.text(), + ty_to_string(&ty) + ); + if let Some(d) = field.doc() { + value + .push_str(&format!("\n\n***\n\n{}\n\n***", d)); + } + return Some(HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value, + })); + } + } } + if let Some((_, n)) = find_declaration(&token) { let text = n .children_with_tokens() diff --git a/ls/src/features/inlay_hint.rs b/ls/src/features/inlay_hint.rs index b4e9e5448..d4fdef0c2 100644 --- a/ls/src/features/inlay_hint.rs +++ b/ls/src/features/inlay_hint.rs @@ -52,7 +52,7 @@ pub fn inlay_hint( && let Some(new_ty) = func .signatures .first() - .map(|sign| &sign.ret) + .map(|sign| sign.ret_type()) { new_ty.clone() } else { @@ -90,8 +90,10 @@ pub fn inlay_hint( { // Extract return type from function. ty = if let Type::Func(func) = &ty - && let Some(new_ty) = - func.signatures.first().map(|sign| &sign.ret) + && let Some(new_ty) = func + .signatures + .first() + .map(|sign| sign.ret_type()) { new_ty.clone() } else { diff --git a/ls/src/features/signature_help.rs b/ls/src/features/signature_help.rs index 84ff37f4a..a567a1b03 100644 --- a/ls/src/features/signature_help.rs +++ b/ls/src/features/signature_help.rs @@ -4,7 +4,7 @@ use crate::{ documents::storage::DocumentStorage, utils::{ cst_traversal::{prev_non_trivia_token, token_at_position}, - modules::{get_struct, ty_to_string}, + modules::{get_type, ty_to_string}, }, }; use async_lsp::lsp_types::{ @@ -54,7 +54,7 @@ pub fn signature_help( let last_ident = curr?; - let func = match get_struct(&last_ident) { + let func = match get_type(&last_ident) { Some(Type::Func(func)) => Some(func), _ => None, }?; @@ -63,16 +63,17 @@ pub fn signature_help( let signature_start = format!("{}(", last_ident.text()); for signature in func.signatures { + let mut args = signature.args(); + // Ignore signatures that have fewer parameters. - if (active_parameter + 1) as usize > signature.args.len() { + if (active_parameter + 1) as usize > args.len() { continue; } let mut curr_signature = signature_start.clone(); - let mut param_iterator = signature.args.iter(); let mut param_info = Vec::new(); - if let Some((name, ty)) = param_iterator.next() { + if let Some((name, ty)) = args.next() { let mut curr_name = name; let mut curr_type = ty; loop { @@ -86,7 +87,7 @@ pub fn signature_help( documentation: None, }); curr_signature.push_str(&ty_str); - if let Some((next_name, next_type)) = param_iterator.next() { + if let Some((next_name, next_type)) = args.next() { curr_signature.push_str(", "); curr_name = next_name; curr_type = next_type; @@ -97,7 +98,7 @@ pub fn signature_help( } curr_signature.push_str(") -> "); - curr_signature.push_str(&ty_to_string(&signature.ret)); + curr_signature.push_str(&ty_to_string(signature.ret_type())); signatures.push(SignatureInformation { label: curr_signature, diff --git a/ls/src/tests/mod.rs b/ls/src/tests/mod.rs index 98b38d315..5ab41ce6f 100644 --- a/ls/src/tests/mod.rs +++ b/ls/src/tests/mod.rs @@ -296,6 +296,7 @@ async fn hover() { test_lsp_request::<_, HoverRequest>("hover8.yar").await; test_lsp_request::<_, HoverRequest>("hover9.yar").await; test_lsp_request::<_, HoverRequest>("hover10.yar").await; + test_lsp_request::<_, HoverRequest>("hover11.yar").await; } #[tokio::test] diff --git a/ls/src/tests/testdata/hover11.request.json b/ls/src/tests/testdata/hover11.request.json new file mode 100644 index 000000000..ccf639850 --- /dev/null +++ b/ls/src/tests/testdata/hover11.request.json @@ -0,0 +1,9 @@ +{ + "textDocument": { + "uri": "${test_dir}/hover11.yar" + }, + "position": { + "line": 4, + "character": 9 + } +} \ No newline at end of file diff --git a/ls/src/tests/testdata/hover11.response.json b/ls/src/tests/testdata/hover11.response.json new file mode 100644 index 000000000..ceaca5cef --- /dev/null +++ b/ls/src/tests/testdata/hover11.response.json @@ -0,0 +1,6 @@ +{ + "contents": { + "kind": "markdown", + "value": "### `is_pe: bool`\n\n***\n\nTrue if the file is a valid PE binary.\n\n***" + } +} \ No newline at end of file diff --git a/ls/src/tests/testdata/hover11.yar b/ls/src/tests/testdata/hover11.yar new file mode 100644 index 000000000..087a0bfc3 --- /dev/null +++ b/ls/src/tests/testdata/hover11.yar @@ -0,0 +1,6 @@ +import "pe" + +rule test { + condition: + pe.is_pe +} diff --git a/ls/src/utils/modules.rs b/ls/src/utils/modules.rs index bfa2fa528..60f239563 100644 --- a/ls/src/utils/modules.rs +++ b/ls/src/utils/modules.rs @@ -24,10 +24,10 @@ pub enum Segment { /// /// Returns an `Option` representing the type of the structure or field /// identified by the token. Returns `None` if the type cannot be determined. -pub fn get_struct(token: &Token) -> Option { +pub fn get_type(token: &Token) -> Option { let mut path = Vec::new(); - let mut curr = Some(token.clone()); + while let Some(token) = curr { match token.kind() { SyntaxKind::IDENT => { @@ -40,7 +40,6 @@ pub fn get_struct(token: &Token) -> Option { path.into_iter().rev(), ); } - path.push(Segment::Field(token.text().to_string())); // Look for previous DOT if let Some(prev) = prev_non_trivia_token(&token) @@ -106,6 +105,7 @@ pub fn get_struct(token: &Token) -> Option { } } } + Some(current_kind) } @@ -143,7 +143,7 @@ pub fn get_type_from_declaration( continue; } - let mut current_type = get_struct(&with_decl.last_token()?)?; + let mut current_type = get_type(&with_decl.last_token()?)?; for segment in path { match segment { @@ -177,8 +177,7 @@ pub fn get_type_from_declaration( .into_token()?; let iterable_last_token = prev_non_trivia_token(&colon)?; - - let iterable_type = get_struct(&iterable_last_token)?; + let iterable_type = get_type(&iterable_last_token)?; let mut current_type = match iterable_type { Type::Array(inner) => *inner, diff --git a/site/content/docs/modules/console.md b/site/content/docs/modules/console.md index 9f1419859..d43d59c55 100644 --- a/site/content/docs/modules/console.md +++ b/site/content/docs/modules/console.md @@ -50,14 +50,22 @@ Logs the given message and string. Example: `console.log("The imphash is: ", pe.imphash())` -### log(offset, length) (New in version v.1.15.0) +### log(offset, length) + +{{< callout >}} +New in version 1.15.0 +{{< /callout >}} Logs the bytes starting at offset and continuing for length. The result is an ASCII escaped string. Example: `console.log(10, 5)` -### log(message, offset, length) (New in version v.1.15.0) +### log(message, offset, length) + +{{< callout >}} +New in version 1.15.0 +{{< /callout >}} Logs the bytes starting at offset and continuing for length. The result is an ASCII escaped string.