Files
Vivek Chavan fa76105d14 fix: detect preprocessor in re-exported named exports (#6899)
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
2025-10-23 07:51:57 +00:00

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));
}
}