Files
windmill/backend/parsers/windmill-parser-go/src/lib.rs
HugoCasa a79e09d65f feat: improve parsers when no main func (#3805)
* feat: improve parsers when no main func

* chore: update parser version
2024-05-23 20:36:16 +02:00

313 lines
9.3 KiB
Rust

#![allow(non_snake_case)] // TODO: switch to parse_* function naming
use gosyn::{
ast::{Declaration, Expression, Field, Ident, StructType},
parse_source,
};
use itertools::Itertools;
use regex::Regex;
use windmill_parser::{Arg, MainArgSignature, ObjectProperty, Typ};
lazy_static::lazy_static! {
pub static ref REQUIRE_PARSE: Regex = Regex::new(r"//require (.*)\n").unwrap();
}
pub fn parse_go_sig(code: &str) -> anyhow::Result<MainArgSignature> {
let filtered_code = filter_non_main(code);
let file = parse_source(&filtered_code).map_err(|x| anyhow::anyhow!(x.to_string()))?;
if let Some(func) = file.decl.iter().find_map(|x| match x {
Declaration::Function(func) if &func.name.name == "main" => Some(func),
_ => None,
}) {
let args = func
.typ
.params
.list
.iter()
.map(|param| {
let (otyp, typ) = parse_go_typ(&param.typ);
Arg { name: get_name(param), otyp, typ, default: None, has_default: false }
})
.collect_vec();
Ok(MainArgSignature {
star_args: false,
star_kwargs: false,
args,
no_main_func: Some(false),
})
} else {
Ok(MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![],
no_main_func: Some(true),
})
}
}
pub fn parse_go_imports(code: &str) -> anyhow::Result<Vec<String>> {
let file =
parse_source(filter_non_imports(code)).map_err(|x| anyhow::anyhow!(x.to_string()))?;
let mut imports: Vec<String> = file
.imports
.iter()
.filter_map(|x| {
if x.path.value.contains("/") {
Some(x.path.value.clone())
} else {
None
}
})
.collect();
imports.sort();
Ok([
imports,
REQUIRE_PARSE
.captures_iter(code)
.map(|x| x[1].to_string())
.collect_vec(),
]
.concat())
}
fn get_name(param: &Field) -> String {
param
.name
.first()
.map(|y| y.name.to_string())
.unwrap_or_else(|| "".to_string())
}
fn parse_go_typ(typ: &Expression) -> (Option<String>, Typ) {
match typ {
Expression::Ident(Ident { name, .. }) => (
Some((name).to_string()),
match name.as_str() {
"int" => Typ::Int,
"int16" => Typ::Int,
"int32" => Typ::Int,
"int64" => Typ::Int,
"string" => Typ::Str(None),
"bool" => Typ::Bool,
_ => Typ::Unknown,
},
),
Expression::TypeSlice(slice_type) => {
let (inner_otyp, inner_typ) = parse_go_typ(&*slice_type.typ);
(
inner_otyp.map(|x| format!("[]{}", x)),
Typ::List(Box::new(inner_typ)),
)
}
Expression::TypeArray(array_type) => {
let (inner_otyp, inner_typ) = parse_go_typ(&*array_type.typ);
(
inner_otyp.map(|x| format!("[]{x}")),
Typ::List(Box::new(inner_typ)),
)
}
Expression::TypeStruct(StructType { fields, .. }) => {
let (otyps, typs): (Vec<String>, Vec<ObjectProperty>) = fields
.iter()
.map(|field| {
let json_tag = field
.tag
.as_ref()
.and_then(|x| x.value.strip_prefix("`json:\""))
.and_then(|x| x.strip_suffix("\"`"))
.and_then(|x| x.split(',').next().map(|x| x.to_string()));
let (otyp, typ) = parse_go_typ(&field.typ);
let name = get_name(field);
let key = json_tag.unwrap_or_else(|| name.to_string());
(
format!("{name} {} `json:\"{key}\"`", otyp_to_string(otyp)),
ObjectProperty { key, typ: Box::new(typ) },
)
})
.collect::<Vec<_>>()
.into_iter()
.unzip();
(
Some(format!(
"struct {{ {} }}",
otyps.iter().join("; ").to_string()
)),
Typ::Object(typs),
)
}
Expression::TypeInterface(_) => (Some("interface{}".to_string()), Typ::Object(vec![])),
Expression::TypeMap(_) => (
Some("map[string]interface{}".to_string()),
Typ::Object(vec![]),
),
_ => (None, Typ::Unknown),
}
}
pub fn otyp_to_string(otyp: Option<String>) -> String {
otyp.unwrap_or_else(|| "interface{}".to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_go_sig() -> anyhow::Result<()> {
let code = r#"
package main
import "fmt"
func main(x int, y string, z bool, l []string, o struct { Name string `json:"name"` }, n interface{}, m map[string]interface{}) {
fmt.Println("hello world")
}
"#;
//println!("{}", serde_json::to_string()?);
assert_eq!(
parse_go_sig(code)?,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![
Arg {
otyp: Some("int".to_string()),
name: "x".to_string(),
typ: Typ::Int,
has_default: false,
default: None
},
Arg {
otyp: Some("string".to_string()),
name: "y".to_string(),
typ: Typ::Str(None),
default: None,
has_default: false
},
Arg {
otyp: Some("bool".to_string()),
name: "z".to_string(),
typ: Typ::Bool,
default: None,
has_default: false
},
Arg {
otyp: Some("[]string".to_string()),
name: "l".to_string(),
typ: Typ::List(Box::new(Typ::Str(None))),
default: None,
has_default: false
},
Arg {
otyp: Some("struct { Name string `json:\"name\"` }".to_string()),
name: "o".to_string(),
typ: Typ::Object(vec![ObjectProperty {
key: "name".to_string(),
typ: Box::new(Typ::Str(None))
},]),
default: None,
has_default: false
},
Arg {
otyp: Some("interface{}".to_string()),
name: "n".to_string(),
typ: Typ::Object(vec![]),
default: None,
has_default: false
},
Arg {
otyp: Some("map[string]interface{}".to_string()),
name: "m".to_string(),
typ: Typ::Object(vec![]),
default: None,
has_default: false
},
],
no_main_func: Some(false)
}
);
Ok(())
}
}
#[test]
fn test_parse_go_import() -> anyhow::Result<()> {
let code = r#"
package inner
import (
"fmt"
"rsc.io/quote"
wmill "github.com/windmill-labs/windmill-go-client"
)
// the main must return (interface{}, error)
func main(x string, nested struct {
Foo string `json:"foo"`
}) (interface{}, error) {
fmt.Println("Hello, World")
fmt.Println(nested.Foo)
fmt.Println(quote.Opt())
v, _ := wmill.GetVariable("f/examples/secret")
return v, nil
}
"#;
assert_eq!(
parse_go_imports(code)?,
vec![
"\"github.com/windmill-labs/windmill-go-client\"",
"\"rsc.io/quote\""
]
);
Ok(())
}
fn filter_non_imports(code: &str) -> String {
code.split_once("func ")
.map(|(x, _)| x.to_string())
.unwrap_or_else(|| code.to_string())
}
fn filter_non_main(code: &str) -> String {
const FUNC_MAIN: &str = "func main(";
let mut filtered_code = "package main;\n".to_string();
let mut code_iter = code.split("\n");
let mut remaining: String = String::new();
while let Some(line) = code_iter.next() {
if line.starts_with(FUNC_MAIN) {
filtered_code += FUNC_MAIN;
remaining += line.strip_prefix(FUNC_MAIN).unwrap();
remaining += &code_iter.join("\n");
break;
}
}
if filtered_code.is_empty() {
return String::new();
}
let mut chars = remaining.chars();
let mut open_parens = 1;
while let Some(c) = chars.next() {
if c == '(' {
open_parens += 1;
} else if c == ')' {
open_parens -= 1;
}
filtered_code.push(c);
if open_parens == 0 {
break;
}
}
filtered_code.push_str("{}");
return filtered_code;
}