Files
windmill/backend/parsers/windmill-parser-rust/src/lib.rs
T
HugoCasa ae8f29a4cb feat: http routing (#4339)
* feat: http routing

* all

* feat: improve UI

* final stuff

* fix: sqlx

* fix: nit

* fix: nits

* fix: error handler display

* fix: routes panel perms

* all

* fix: improve ability to paste from macos in vscode extension

* fix lock-write for deno

* all

* cleaning

* fix

* cli preprocessor

* nits

* nits

---------

Co-authored-by: Ruben Fiszel <ruben@rubenfiszel.com>
2024-09-20 15:38:38 +02:00

501 lines
15 KiB
Rust

use anyhow::anyhow;
use itertools::Itertools;
use quote::ToTokens;
use regex::Regex;
use windmill_parser::{Arg, MainArgSignature, Typ};
pub fn otyp_to_string(otyp: Option<String>) -> String {
otyp.unwrap()
}
pub fn parse_rust_signature(code: &str) -> anyhow::Result<MainArgSignature> {
let ast: syn::File = syn::parse_file(code)?;
if let Some(main_fn) = ast.items.iter().find_map(|item| match item {
syn::Item::Fn(f) if f.sig.ident == "main" => Some(f),
_ => None,
}) {
let args = main_fn
.sig
.inputs
.iter()
.map(|param| {
let (otyp, typ, name) = parse_rust_typ(param);
Arg { name, otyp, typ, default: None, has_default: false, oidx: None }
})
.collect_vec();
Ok(MainArgSignature {
star_args: false,
star_kwargs: false,
args,
no_main_func: Some(false),
has_preprocessor: None,
})
} else {
Ok(MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![],
no_main_func: Some(true),
has_preprocessor: None,
})
}
}
pub fn parse_rust_deps_into_manifest(code: &str) -> anyhow::Result<String> {
const MODIFIABLE_MANIFEST_TABLES: &[&str] = &["dependencies"];
let partial_manifest = find_embedded_manifest(code).unwrap_or(Manifest::Toml("".to_string()));
let partial_manifest = partial_manifest.into_toml()?;
let mut manif = default_manifest();
for table_name in MODIFIABLE_MANIFEST_TABLES {
match partial_manifest.get(*table_name) {
Some(toml::Value::Table(tab)) => {
// Merge.
match manif.entry(*table_name) {
toml::map::Entry::Vacant(e) => {
e.insert(toml::Value::Table(tab.to_owned()));
}
toml::map::Entry::Occupied(e) => {
let into_t = match e.into_mut() {
toml::Value::Table(t) => Some(t),
_ => None,
};
into_t.ok_or(anyhow!(""))?.extend(tab.to_owned());
}
}
}
Some(v) => {
// Just replace.
manif.insert(table_name.to_string(), v.to_owned());
}
None => (),
}
}
Ok(manif.to_string())
}
fn parse_pat_type(p: Box<syn::Type>) -> Typ {
match *p {
syn::Type::Array(a) => {
let inner_typ = parse_pat_type(a.elem);
Typ::List(Box::new(inner_typ))
}
// syn::Type::BareFn(_) => todo!(),
// syn::Type::Group(_) => todo!(),
// syn::Type::ImplTrait(_) => todo!(),
// syn::Type::Infer(_) => todo!(),
// syn::Type::Macro(_) => todo!(),
// syn::Type::Never(_) => todo!(),
syn::Type::Paren(e) => parse_pat_type(e.elem),
syn::Type::Path(e) => {
if let Some(u) = e.path.segments.last() {
match u.ident.to_string().as_str() {
"usize" | "u8" | "u16" | "u32" | "u64" | "u128" | "isize" | "i8" | "i16"
| "i32" | "i64" | "i128" => Typ::Int,
"String" | "str" => Typ::Str(None),
"bool" => Typ::Bool,
"f32" | "f64" => Typ::Float,
"Vec" => {
if let syn::PathArguments::AngleBracketed(t) = &u.arguments {
if let Some(syn::GenericArgument::Type(a)) = t.args.last() {
Typ::List(Box::new(parse_pat_type(Box::new(a.clone()))))
} else {
Typ::Unknown
}
} else {
Typ::Unknown
}
}
_ => Typ::Unknown,
}
} else {
Typ::Unknown
}
}
// syn::Type::Ptr(_) => todo!(),
syn::Type::Reference(e) => parse_pat_type(e.elem),
syn::Type::Slice(e) => Typ::List(Box::new(parse_pat_type(e.elem))),
// syn::Type::TraitObject(_) => todo!(),
// syn::Type::Tuple(_) => todo!(),
// syn::Type::Verbatim(_) => todo!(),
_ => Typ::Unknown,
}
}
fn parse_rust_typ(param_typ: &syn::FnArg) -> (Option<String>, Typ, String) {
match param_typ {
syn::FnArg::Receiver(_) => (None, Typ::Unknown, "self".to_string()),
syn::FnArg::Typed(s) => {
let name = match *s.pat.clone() {
syn::Pat::Ident(p) => p.ident.to_string(),
_ => "undefined".to_string(),
};
let otyp = Some(s.ty.to_token_stream().to_string());
let typ = parse_pat_type(s.ty.clone());
(otyp, typ, name)
}
}
}
#[derive(Debug, PartialEq)]
enum Manifest {
Toml(String),
DepList(String),
}
impl Manifest {
pub fn into_toml(self) -> anyhow::Result<toml::value::Table> {
match self {
Manifest::Toml(s) => toml::from_str(&s),
Manifest::DepList(s) => Manifest::dep_list_to_toml(&s),
}
.map_err(|e| anyhow!("Could not parse embedded manifest: {}", e))
}
fn dep_list_to_toml(s: &str) -> ::std::result::Result<toml::value::Table, toml::de::Error> {
let mut r = String::new();
r.push_str("[dependencies]\n");
for dep in s.trim().split(',') {
// If there's no version specified, add one.
match dep.contains('=') {
true => {
r.push_str(dep);
r.push('\n');
}
false => {
r.push_str(dep);
r.push_str("=\"*\"\n");
}
}
}
toml::from_str(&r)
}
}
fn default_manifest() -> toml::value::Table {
toml::from_str(include_str!("../manifest/Cargo.toml.default"))
.expect("Failed to parse Cargo.toml.default")
}
/**
* Get a `Manifest` that can be made into a TOML table. The format and logic are the same as in
* the rust-script or cargo-eval projects.
*/
fn find_embedded_manifest(s: &str) -> Option<Manifest> {
find_short_hand_manifest(s).or_else(|| find_code_block_manifest(s))
}
fn find_short_hand_manifest(s: &str) -> Option<Manifest> {
let re: Regex = Regex::new(r"^(?i)\s*//\s*cargo-deps\s*:(.*?)(\r\n|\n)").unwrap();
/*
This is pretty simple: the only valid syntax for this is for the first, non-blank line to contain a single-line comment whose first token is `cargo-deps:`. That's it.
*/
if let Some(cap) = re.captures(s) {
if let Some(m) = cap.get(1) {
return Some(Manifest::DepList(m.as_str().to_string()));
}
}
None
}
/**
Locates a "code block manifest" in Rust source.
*/
fn find_code_block_manifest(s: &str) -> Option<Manifest> {
let re_crate_comment: Regex = {
Regex::new(
r"(?x)
^\s*
(/\*!|//(!|/))
",
)
.unwrap()
};
let start = match re_crate_comment.captures(s) {
Some(cap) => match cap.get(1) {
Some(m) => m.start(),
None => return None,
},
None => return None,
};
let comment = match extract_comment(&s[start..]) {
Some(s) => s,
None => {
return None;
}
};
scrape_markdown_manifest(&comment).map(|s| Manifest::Toml(s))
}
/**
Extracts the first `Cargo` fenced code block from a chunk of Markdown.
*/
fn scrape_markdown_manifest(content: &str) -> Option<String> {
use pulldown_cmark::{CodeBlockKind, Event, Options, Parser, Tag};
// To match librustdoc/html/markdown.rs, opts.
let exts = Options::ENABLE_TABLES | Options::ENABLE_FOOTNOTES;
let md = Parser::new_ext(content, exts);
let mut found = false;
let mut output = None;
for item in md {
match item {
Event::Start(Tag::CodeBlock(CodeBlockKind::Fenced(ref info)))
if info.to_lowercase() == "cargo" && output.is_none() =>
{
found = true;
}
Event::Text(ref text) if found => {
let s = output.get_or_insert(String::new());
s.push_str(text);
}
Event::End(Tag::CodeBlock(_)) if found => {
found = false;
}
_ => (),
}
}
output
}
/**
Extracts the contents of a Rust doc comment.
*/
fn extract_comment(s: &str) -> Option<String> {
use std::cmp::min;
fn extract_block(s: &str) -> Option<String> {
/*
On every line:
- update nesting level and detect end-of-comment
- if margin is None:
- if there appears to be a margin, set margin.
- strip off margin marker
- update the leading space counter
- strip leading space
- append content
*/
let mut r = String::new();
let margin_re: Regex = Regex::new(r"^\s*\*( |$)").unwrap();
let space_re: Regex = Regex::new(r"^(\s+)").unwrap();
let nesting_re: Regex = Regex::new(r"/\*|\*/").unwrap();
let mut leading_space = None;
let mut margin = None;
let mut depth: u32 = 1;
for line in s.lines() {
if depth == 0 {
break;
}
let mut end_of_comment = None;
for (end, marker) in nesting_re.find_iter(line).map(|m| (m.start(), m.as_str())) {
match (marker, depth) {
("/*", _) => depth += 1,
("*/", 1) => {
end_of_comment = Some(end);
depth = 0;
break;
}
("*/", _) => depth -= 1,
_ => return None,
}
}
let line = end_of_comment.map(|end| &line[..end]).unwrap_or(line);
margin = margin.or_else(|| margin_re.find(line).map(|m| m.as_str()));
let line = if let Some(margin) = margin {
let end = line
.char_indices()
.take(margin.len())
.map(|(i, c)| i + c.len_utf8())
.last()
.unwrap_or(0);
&line[end..]
} else {
line
};
leading_space = leading_space.or_else(|| space_re.find(line).map(|m| m.end()));
let strip_len = min(leading_space.unwrap_or(0), line.len());
let line = &line[strip_len..];
r.push_str(line);
r.push('\n');
}
Some(r)
}
fn extract_line(s: &str) -> Option<String> {
let mut r = String::new();
let comment_re = Regex::new(r"^\s*//(!|/)").unwrap();
let space_re = Regex::new(r"^(\s+)").unwrap();
let mut leading_space = None;
for line in s.lines() {
let content = match comment_re.find(line) {
Some(m) => &line[m.end()..],
None => break,
};
leading_space = leading_space.or_else(|| {
space_re
.captures(content)
.and_then(|c| c.get(1))
.map(|m| m.end())
});
let strip_len = min(leading_space.unwrap_or(0), content.len());
let content = &content[strip_len..];
r.push_str(content);
r.push('\n');
}
Some(r)
}
if let Some(stripped) = s.strip_prefix("/*!") {
extract_block(stripped)
} else if s.starts_with("//!") || s.starts_with("///") {
extract_line(s)
} else {
None
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_parse_rust_signature() {
let code = r#"
// commenting comments
fn main(
my_int: i8,
my_vec: Vec<u8>,
mut my_arr: [u8; 9],
my_complex: Vec<Result<MyStruct, anyhow::Error>>,
) -> Result<String, String> {
println!("My int is {}", my_int);
}"#;
let ret = parse_rust_signature(code).unwrap();
assert_eq!(ret.args.len(), 4);
assert_eq!(ret.args[0].name, "my_int");
assert_eq!(ret.args[0].otyp, Some("i8".to_string()));
assert_eq!(ret.args[0].typ, Typ::Int);
assert_eq!(ret.args[1].name, "my_vec");
assert_eq!(ret.args[1].otyp, Some("Vec < u8 >".to_string()));
assert_eq!(ret.args[1].typ, Typ::List(Box::new(Typ::Int)));
assert_eq!(ret.args[2].name, "my_arr");
assert_eq!(ret.args[2].otyp, Some("[u8 ; 9]".to_string()));
assert_eq!(ret.args[2].typ, Typ::List(Box::new(Typ::Int)));
assert_eq!(ret.args[3].name, "my_complex");
assert_eq!(
ret.args[3].otyp,
Some("Vec < Result < MyStruct , anyhow :: Error > >".to_string())
);
assert_eq!(ret.args[3].typ, Typ::List(Box::new(Typ::Unknown)));
let code = r#"
// commenting comments
fn main(
my_str_slice: &str,
my_String: String,
mut my_mut_ref_to_string: &mut String,
my_string_vec: Vec<String>,
) -> Result<String, String> {
println!("My int is {}", my_int);
}"#;
let ret = parse_rust_signature(code).unwrap();
assert_eq!(ret.args.len(), 4);
assert_eq!(ret.args[0].name, "my_str_slice");
assert_eq!(ret.args[0].otyp, Some("& str".to_string()));
assert_eq!(ret.args[0].typ, Typ::Str(None));
assert_eq!(ret.args[1].name, "my_String");
assert_eq!(ret.args[1].otyp, Some("String".to_string()));
assert_eq!(ret.args[1].typ, Typ::Str(None));
assert_eq!(ret.args[2].name, "my_mut_ref_to_string");
assert_eq!(ret.args[2].otyp, Some("& mut String".to_string()));
assert_eq!(ret.args[2].typ, Typ::Str(None));
assert_eq!(ret.args[3].name, "my_string_vec");
assert_eq!(ret.args[3].otyp, Some("Vec < String >".to_string()));
assert_eq!(ret.args[3].typ, Typ::List(Box::new(Typ::Str(None))));
}
#[test]
fn test_parse_rust_manifest() {
assert_eq!(find_embedded_manifest("fn main() {}"), None);
let code = r#"
//! Crate doc comment right here
//! We need to use the `cargo` language in the code bloc
//!
//! ```cargo
//! [dependencies]
//! time = "0.1.25"
//! ```
fn main() {
println!("{}", time::now().rfc822z());
}
"#;
let manif = find_embedded_manifest(code);
assert_eq!(
manif,
Some(Manifest::Toml(
r#"[dependencies]
time = "0.1.25"
"#
.to_string()
))
);
let _full_manif = parse_rust_deps_into_manifest(code).unwrap();
}
#[test]
fn test_default_manifest() {
default_manifest();
}
}