Fixes #6894 The TypeScript parser now correctly detects preprocessor functions that are re-exported from other modules using named exports like: export { preprocessor } from "./other_module"; Previously, only function declarations were detected. Now the parser also checks ExportNamed AST nodes for any specifier named 'preprocessor'. This allows developers to easily reuse preprocessor functions across multiple scripts without the workaround of wrapping them in a new function. Added comprehensive tests covering: - Simple re-export: export { preprocessor } from "./other" - Re-export with renaming: export { preprocessor as preprocessor } - Mixed exports: export { foo, preprocessor, bar } - Negative case: exports without preprocessor
761 lines
24 KiB
Rust
761 lines
24 KiB
Rust
#[cfg(test)]
|
|
mod tests {
|
|
use serde_json::json;
|
|
use windmill_parser::{Arg, MainArgSignature, ObjectProperty, ObjectType, Typ};
|
|
use windmill_parser_ts::{parse_deno_signature, parse_expr_for_imports};
|
|
|
|
#[test]
|
|
fn test_imports_basic() {
|
|
let code = r#"
|
|
import { foo } from "bar";
|
|
import type { foo } from "bar2";
|
|
import { type foo, bar } from "bar3";
|
|
import { bar, type foo } from "bar7";
|
|
|
|
import { type foo, type bar } from "bar4";
|
|
import * as foo from "bar5";
|
|
import foo from "bar6";
|
|
export * from "bar8";
|
|
export { foo } from "bar9";
|
|
export { type foo } from "bar10";
|
|
export { foo, type bar } from "bar11";
|
|
export { type foo, type bar } from "bar12";
|
|
export { type foo, bar } from "bar13";
|
|
export { bar, type foo } from "bar14";
|
|
export type { foo } from "bar15";
|
|
"#;
|
|
let imports = parse_expr_for_imports(code, true).unwrap();
|
|
assert_eq!(
|
|
imports,
|
|
["bar", "bar11", "bar13", "bar14", "bar3", "bar5", "bar6", "bar7", "bar8", "bar9"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_empty_main_signature() {
|
|
let code = r#"
|
|
export async function main() {
|
|
return "Hello World";
|
|
}
|
|
"#;
|
|
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
|
assert_eq!(
|
|
sig,
|
|
MainArgSignature {
|
|
star_args: false,
|
|
star_kwargs: false,
|
|
args: vec![],
|
|
no_main_func: Some(false),
|
|
has_preprocessor: Some(false),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_basic_types() {
|
|
let code = r#"
|
|
export async function main(
|
|
str_param: string,
|
|
num_param: number,
|
|
bool_param: boolean,
|
|
any_param: any
|
|
) {
|
|
return { str_param, num_param, bool_param, any_param };
|
|
}
|
|
"#;
|
|
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
|
assert_eq!(
|
|
sig,
|
|
MainArgSignature {
|
|
star_args: false,
|
|
star_kwargs: false,
|
|
args: vec![
|
|
Arg {
|
|
name: "str_param".to_string(),
|
|
otyp: None,
|
|
typ: Typ::Str(None),
|
|
default: None,
|
|
has_default: false,
|
|
oidx: None,
|
|
},
|
|
Arg {
|
|
name: "num_param".to_string(),
|
|
otyp: None,
|
|
typ: Typ::Float,
|
|
default: None,
|
|
has_default: false,
|
|
oidx: None,
|
|
},
|
|
Arg {
|
|
name: "bool_param".to_string(),
|
|
otyp: None,
|
|
typ: Typ::Bool,
|
|
default: None,
|
|
has_default: false,
|
|
oidx: None,
|
|
},
|
|
Arg {
|
|
name: "any_param".to_string(),
|
|
otyp: None,
|
|
typ: Typ::Unknown,
|
|
default: None,
|
|
has_default: false,
|
|
oidx: None,
|
|
},
|
|
],
|
|
no_main_func: Some(false),
|
|
has_preprocessor: Some(false),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_with_defaults() {
|
|
let code = r#"
|
|
export async function main(
|
|
name: string = "World",
|
|
count: number = 42,
|
|
enabled: boolean = true
|
|
) {
|
|
return { name, count, enabled };
|
|
}
|
|
"#;
|
|
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
|
assert_eq!(
|
|
sig,
|
|
MainArgSignature {
|
|
star_args: false,
|
|
star_kwargs: false,
|
|
args: vec![
|
|
Arg {
|
|
name: "name".to_string(),
|
|
otyp: None,
|
|
typ: Typ::Str(None),
|
|
default: Some(json!("World")),
|
|
has_default: true,
|
|
oidx: None,
|
|
},
|
|
Arg {
|
|
name: "count".to_string(),
|
|
otyp: None,
|
|
typ: Typ::Float,
|
|
default: Some(json!(42)),
|
|
has_default: true,
|
|
oidx: None,
|
|
},
|
|
Arg {
|
|
name: "enabled".to_string(),
|
|
otyp: None,
|
|
typ: Typ::Bool,
|
|
default: Some(json!(true)),
|
|
has_default: true,
|
|
oidx: None,
|
|
},
|
|
],
|
|
no_main_func: Some(false),
|
|
has_preprocessor: Some(false),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_array_types() {
|
|
let code = r#"
|
|
export async function main(
|
|
strings: string[],
|
|
numbers: number[],
|
|
items: any[]
|
|
) {
|
|
return { strings, numbers, items };
|
|
}
|
|
"#;
|
|
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
|
assert_eq!(
|
|
sig,
|
|
MainArgSignature {
|
|
star_args: false,
|
|
star_kwargs: false,
|
|
args: vec![
|
|
Arg {
|
|
name: "strings".to_string(),
|
|
otyp: None,
|
|
typ: Typ::List(Box::new(Typ::Str(None))),
|
|
default: None,
|
|
has_default: false,
|
|
oidx: None,
|
|
},
|
|
Arg {
|
|
name: "numbers".to_string(),
|
|
otyp: None,
|
|
typ: Typ::List(Box::new(Typ::Float)),
|
|
default: None,
|
|
has_default: false,
|
|
oidx: None,
|
|
},
|
|
Arg {
|
|
name: "items".to_string(),
|
|
otyp: None,
|
|
typ: Typ::List(Box::new(Typ::Unknown)),
|
|
default: None,
|
|
has_default: false,
|
|
oidx: None,
|
|
},
|
|
],
|
|
no_main_func: Some(false),
|
|
has_preprocessor: Some(false),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_resource_types() {
|
|
let code = r#"
|
|
type Postgresql = Resource;
|
|
|
|
export async function main(
|
|
database: Postgresql
|
|
) {
|
|
return { database };
|
|
}
|
|
"#;
|
|
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
|
assert_eq!(
|
|
sig,
|
|
MainArgSignature {
|
|
star_args: false,
|
|
star_kwargs: false,
|
|
args: vec![Arg {
|
|
name: "database".to_string(),
|
|
otyp: None,
|
|
typ: Typ::Resource("postgresql".to_string()),
|
|
default: None,
|
|
has_default: false,
|
|
oidx: None,
|
|
},],
|
|
no_main_func: Some(false),
|
|
has_preprocessor: Some(false),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_array_resource_types() {
|
|
let code = r#"
|
|
type Postgresql = Foo;
|
|
|
|
export async function main(
|
|
database: Postgresql[]
|
|
) {
|
|
return { database };
|
|
}
|
|
"#;
|
|
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
|
assert_eq!(
|
|
sig,
|
|
MainArgSignature {
|
|
star_args: false,
|
|
star_kwargs: false,
|
|
args: vec![Arg {
|
|
name: "database".to_string(),
|
|
otyp: None,
|
|
typ: Typ::List(Box::new(Typ::Resource("postgresql".to_string()))),
|
|
default: None,
|
|
has_default: false,
|
|
oidx: None,
|
|
},],
|
|
no_main_func: Some(false),
|
|
has_preprocessor: Some(false),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_interface_object() {
|
|
let code = r#"
|
|
interface UserConfig {
|
|
name: string;
|
|
age: number;
|
|
active?: boolean;
|
|
}
|
|
|
|
export async function main(config: UserConfig) {
|
|
return config;
|
|
}
|
|
"#;
|
|
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
|
assert_eq!(
|
|
sig,
|
|
MainArgSignature {
|
|
star_args: false,
|
|
star_kwargs: false,
|
|
args: vec![Arg {
|
|
name: "config".to_string(),
|
|
otyp: None,
|
|
typ: Typ::Object(ObjectType::new(
|
|
Some("user_config".to_string()),
|
|
Some(vec![
|
|
ObjectProperty::new("name".to_string(), Box::new(Typ::Str(None))),
|
|
ObjectProperty::new("age".to_string(), Box::new(Typ::Float)),
|
|
ObjectProperty::new("active".to_string(), Box::new(Typ::Bool)),
|
|
])
|
|
)),
|
|
default: None,
|
|
has_default: false,
|
|
oidx: None,
|
|
}],
|
|
no_main_func: Some(false),
|
|
has_preprocessor: Some(false),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_array_of_objects() {
|
|
let code = r#"
|
|
interface Item {
|
|
id: string;
|
|
value: number;
|
|
}
|
|
|
|
export async function main(items: Item[]) {
|
|
return items;
|
|
}
|
|
"#;
|
|
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
|
assert_eq!(
|
|
sig,
|
|
MainArgSignature {
|
|
star_args: false,
|
|
star_kwargs: false,
|
|
args: vec![Arg {
|
|
name: "items".to_string(),
|
|
otyp: None,
|
|
typ: Typ::List(Box::new(Typ::Object(ObjectType::new(
|
|
Some("item".to_string()),
|
|
Some(vec![
|
|
ObjectProperty::new("id".to_string(), Box::new(Typ::Str(None))),
|
|
ObjectProperty::new("value".to_string(), Box::new(Typ::Float)),
|
|
])
|
|
)))),
|
|
default: None,
|
|
has_default: false,
|
|
oidx: None,
|
|
}],
|
|
no_main_func: Some(false),
|
|
has_preprocessor: Some(false),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_nested_objects() {
|
|
let code = r#"
|
|
interface Address {
|
|
street: string;
|
|
city: string;
|
|
}
|
|
|
|
interface Person {
|
|
name: string;
|
|
address: Address;
|
|
}
|
|
|
|
export async function main(person: Person) {
|
|
return person;
|
|
}
|
|
"#;
|
|
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
|
assert_eq!(
|
|
sig,
|
|
MainArgSignature {
|
|
star_args: false,
|
|
star_kwargs: false,
|
|
args: vec![Arg {
|
|
name: "person".to_string(),
|
|
otyp: None,
|
|
typ: Typ::Object(ObjectType::new(
|
|
Some("person".to_string()),
|
|
Some(vec![
|
|
ObjectProperty::new("name".to_string(), Box::new(Typ::Str(None))),
|
|
ObjectProperty::new(
|
|
"address".to_string(),
|
|
Box::new(Typ::Object(ObjectType::new(
|
|
Some("address".to_string()),
|
|
Some(vec![
|
|
ObjectProperty::new(
|
|
"street".to_string(),
|
|
Box::new(Typ::Str(None))
|
|
),
|
|
ObjectProperty::new(
|
|
"city".to_string(),
|
|
Box::new(Typ::Str(None))
|
|
),
|
|
])
|
|
)))
|
|
),
|
|
])
|
|
)),
|
|
default: None,
|
|
has_default: false,
|
|
oidx: None,
|
|
}],
|
|
no_main_func: Some(false),
|
|
has_preprocessor: Some(false),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_builtin_types() {
|
|
let code = r#"
|
|
export async function main(
|
|
date_param: Date,
|
|
base64_param: Base64,
|
|
email_param: Email,
|
|
sql_param: Sql
|
|
) {
|
|
return { date_param, base64_param, email_param, sql_param };
|
|
}
|
|
"#;
|
|
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
|
assert_eq!(
|
|
sig,
|
|
MainArgSignature {
|
|
star_args: false,
|
|
star_kwargs: false,
|
|
args: vec![
|
|
Arg {
|
|
name: "date_param".to_string(),
|
|
otyp: None,
|
|
typ: Typ::Datetime,
|
|
default: None,
|
|
has_default: false,
|
|
oidx: None,
|
|
},
|
|
Arg {
|
|
name: "base64_param".to_string(),
|
|
otyp: None,
|
|
typ: Typ::Bytes,
|
|
default: None,
|
|
has_default: false,
|
|
oidx: None,
|
|
},
|
|
Arg {
|
|
name: "email_param".to_string(),
|
|
otyp: None,
|
|
typ: Typ::Email,
|
|
default: None,
|
|
has_default: false,
|
|
oidx: None,
|
|
},
|
|
Arg {
|
|
name: "sql_param".to_string(),
|
|
otyp: None,
|
|
typ: Typ::Sql,
|
|
default: None,
|
|
has_default: false,
|
|
oidx: None,
|
|
},
|
|
],
|
|
no_main_func: Some(false),
|
|
has_preprocessor: Some(false),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_optional_parameters() {
|
|
let code = r#"
|
|
export async function main(
|
|
required: string,
|
|
optional?: number,
|
|
with_default: boolean = false
|
|
) {
|
|
return { required, optional, with_default };
|
|
}
|
|
"#;
|
|
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
|
assert_eq!(
|
|
sig,
|
|
MainArgSignature {
|
|
star_args: false,
|
|
star_kwargs: false,
|
|
args: vec![
|
|
Arg {
|
|
name: "required".to_string(),
|
|
otyp: None,
|
|
typ: Typ::Str(None),
|
|
default: None,
|
|
has_default: false,
|
|
oidx: None,
|
|
},
|
|
Arg {
|
|
name: "optional".to_string(),
|
|
otyp: None,
|
|
typ: Typ::Float,
|
|
default: None,
|
|
has_default: true,
|
|
oidx: None,
|
|
},
|
|
Arg {
|
|
name: "with_default".to_string(),
|
|
otyp: None,
|
|
typ: Typ::Bool,
|
|
default: Some(json!(false)),
|
|
has_default: true,
|
|
oidx: None,
|
|
},
|
|
],
|
|
no_main_func: Some(false),
|
|
has_preprocessor: Some(false),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_complex_interface() {
|
|
let code = r#"
|
|
interface Config {
|
|
timeout: number;
|
|
retries: number;
|
|
}
|
|
|
|
export async function main(
|
|
config: Config,
|
|
tags: string[]
|
|
) {
|
|
return { config, tags };
|
|
}
|
|
"#;
|
|
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
|
|
|
assert_eq!(sig.args.len(), 2);
|
|
assert_eq!(sig.args[0].name, "config");
|
|
assert_eq!(sig.args[0].has_default, false);
|
|
assert!(matches!(sig.args[0].typ, Typ::Object(_)));
|
|
|
|
assert_eq!(sig.args[1].name, "tags");
|
|
assert_eq!(sig.args[1].has_default, false);
|
|
assert!(matches!(sig.args[1].typ, Typ::List(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_no_main_function() {
|
|
let code = r#"
|
|
function helper() {
|
|
return "helper";
|
|
}
|
|
|
|
const value = 42;
|
|
"#;
|
|
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
|
assert_eq!(
|
|
sig,
|
|
MainArgSignature {
|
|
star_args: false,
|
|
star_kwargs: false,
|
|
args: vec![],
|
|
no_main_func: Some(true),
|
|
has_preprocessor: Some(false),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_custom_entrypoint() {
|
|
let code = r#"
|
|
export async function customMain(param: string) {
|
|
return param;
|
|
}
|
|
"#;
|
|
let sig = parse_deno_signature(code, false, false, Some("customMain".to_string())).unwrap();
|
|
assert_eq!(
|
|
sig,
|
|
MainArgSignature {
|
|
star_args: false,
|
|
star_kwargs: false,
|
|
args: vec![Arg {
|
|
name: "param".to_string(),
|
|
otyp: None,
|
|
typ: Typ::Str(None),
|
|
default: None,
|
|
has_default: false,
|
|
oidx: None,
|
|
}],
|
|
no_main_func: Some(false),
|
|
has_preprocessor: Some(false),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_dyn_select() {
|
|
let code = r#"
|
|
export async function main(
|
|
selector: DynSelect_my_options
|
|
) {
|
|
return selector;
|
|
}
|
|
"#;
|
|
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
|
assert_eq!(
|
|
sig,
|
|
MainArgSignature {
|
|
star_args: false,
|
|
star_kwargs: false,
|
|
args: vec![Arg {
|
|
name: "selector".to_string(),
|
|
otyp: None,
|
|
typ: Typ::DynSelect("my_options".to_string()),
|
|
default: None,
|
|
has_default: false,
|
|
oidx: None,
|
|
}],
|
|
no_main_func: Some(false),
|
|
has_preprocessor: Some(false),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_dyn_multiselect() {
|
|
let code = r#"
|
|
export async function main(
|
|
selector: DynMultiselect_my_options
|
|
) {
|
|
return selector;
|
|
}
|
|
"#;
|
|
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
|
assert_eq!(
|
|
sig,
|
|
MainArgSignature {
|
|
star_args: false,
|
|
star_kwargs: false,
|
|
args: vec![Arg {
|
|
name: "selector".to_string(),
|
|
otyp: None,
|
|
typ: Typ::DynMultiselect("my_options".to_string()),
|
|
default: None,
|
|
has_default: false,
|
|
oidx: None,
|
|
}],
|
|
no_main_func: Some(false),
|
|
has_preprocessor: Some(false),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_invalid_typescript() {
|
|
let code = r#"
|
|
this is not valid typescript code {
|
|
"#;
|
|
let result = parse_deno_signature(code, false, false, None);
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_with_preprocessor() {
|
|
let code = r#"
|
|
export async function preprocessor() {
|
|
return "preprocessed";
|
|
}
|
|
|
|
export async function main(param: string) {
|
|
return param;
|
|
}
|
|
"#;
|
|
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
|
assert_eq!(
|
|
sig,
|
|
MainArgSignature {
|
|
star_args: false,
|
|
star_kwargs: false,
|
|
args: vec![Arg {
|
|
name: "param".to_string(),
|
|
otyp: None,
|
|
typ: Typ::Str(None),
|
|
default: None,
|
|
has_default: false,
|
|
oidx: None,
|
|
}],
|
|
no_main_func: Some(false),
|
|
has_preprocessor: Some(true),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_with_preprocessor_reexport() {
|
|
// Test case for issue #6894: preprocessor re-export should be detected
|
|
let code = r#"
|
|
export { preprocessor } from "./extract_user_info_from_jwt_token";
|
|
|
|
export async function main(param: string) {
|
|
return param;
|
|
}
|
|
"#;
|
|
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
|
assert_eq!(
|
|
sig,
|
|
MainArgSignature {
|
|
star_args: false,
|
|
star_kwargs: false,
|
|
args: vec![Arg {
|
|
name: "param".to_string(),
|
|
otyp: None,
|
|
typ: Typ::Str(None),
|
|
default: None,
|
|
has_default: false,
|
|
oidx: None,
|
|
}],
|
|
no_main_func: Some(false),
|
|
has_preprocessor: Some(true),
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_with_preprocessor_reexport_renamed() {
|
|
// Test case for renamed re-exports
|
|
let code = r#"
|
|
export { preprocessor as preprocessor } from "./other";
|
|
|
|
export async function main(param: string) {
|
|
return param;
|
|
}
|
|
"#;
|
|
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
|
assert_eq!(sig.has_preprocessor, Some(true));
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_with_preprocessor_among_other_exports() {
|
|
// Test case where preprocessor is one of many exports
|
|
let code = r#"
|
|
export { foo, preprocessor, bar } from "./utils";
|
|
|
|
export async function main(param: string) {
|
|
return param;
|
|
}
|
|
"#;
|
|
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
|
assert_eq!(sig.has_preprocessor, Some(true));
|
|
}
|
|
|
|
#[test]
|
|
fn test_parse_without_preprocessor_other_exports() {
|
|
// Test case where there are exports but no preprocessor
|
|
let code = r#"
|
|
export { foo, bar } from "./utils";
|
|
|
|
export async function main(param: string) {
|
|
return param;
|
|
}
|
|
"#;
|
|
let sig = parse_deno_signature(code, false, false, None).unwrap();
|
|
assert_eq!(sig.has_preprocessor, Some(false));
|
|
}
|
|
}
|