@tishlang/tish-lint 2.36.2 → 2.37.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/tish-lint +0 -0
- package/crates/tish_compile/src/infer.rs +521 -17
- package/crates/tish_compile/src/types.rs +7 -2
- package/crates/tish_compile/tests/regr_484_obj_field_param.rs +18 -0
- package/crates/tish_compile/tests/regr_e0382_typed_struct.rs +26 -0
- package/crates/tish_compile/tests/regr_numeric_arg_whitelist.rs +29 -0
- package/crates/tish_lint/Cargo.toml +1 -1
- package/crates/tish_runtime/Cargo.toml +1 -1
- package/crates/tish_runtime/src/http.rs +51 -0
- package/crates/tish_runtime/src/http_fetch.rs +50 -6
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-lint +0 -0
- package/platform/darwin-x64/tish-lint +0 -0
- package/platform/linux-arm64/tish-lint +0 -0
- package/platform/linux-x64/tish-lint +0 -0
- package/platform/win32-x64/tish-lint.exe +0 -0
package/bin/tish-lint
CHANGED
|
Binary file
|
|
@@ -283,7 +283,7 @@ fn param_infer_program(program: Program) -> Program {
|
|
|
283
283
|
// numeric" (`return SHARED`) yet the caller passes e.g. `"arg"`, which the f64 coercion would
|
|
284
284
|
// panic on / turn to NaN where JS keeps the value. Skip those fns (they stay fully boxed). Same
|
|
285
285
|
// set `collect_native_fns` uses to refuse the M5 native form.
|
|
286
|
-
let nonnumeric =
|
|
286
|
+
let nonnumeric = fns_with_unsafe_numeric_arg(&program.statements);
|
|
287
287
|
// Soundness (#477): a fn referenced as a first-class VALUE — passed as a call ARGUMENT
|
|
288
288
|
// (`serve(port, handler)`), assigned, returned, or stored, i.e. any use that is NOT a direct
|
|
289
289
|
// `f(...)` call — escapes into the boxed `Value::Function` path and is later invoked via
|
|
@@ -322,6 +322,522 @@ fn escaping_fn_names(statements: &[Statement]) -> HashSet<String> {
|
|
|
322
322
|
out
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
+
/// Shared candidate predicate: would param `pname` (unannotated, default-less) of a fn with `body`
|
|
326
|
+
/// be M4-promoted to `: number` on the basis of its BODY uses (the #172 optimistic per-param
|
|
327
|
+
/// fixpoint)? Used by BOTH pi_stmt (the promotion) and `numeric_candidate_positions` (the call-arg
|
|
328
|
+
/// whitelist) so the two agree on exactly which positions are numeric candidates.
|
|
329
|
+
fn param_is_body_numeric_candidate(body: &Statement, base_nums: &HashSet<String>, pname: &str) -> bool {
|
|
330
|
+
let mut nums = base_nums.clone();
|
|
331
|
+
nums.insert(pname.to_string());
|
|
332
|
+
collect_numeric_locals_fixpoint(body, &mut nums);
|
|
333
|
+
let copy_descendants: Vec<&String> = nums
|
|
334
|
+
.iter()
|
|
335
|
+
.filter(|x| x.as_str() != pname && !base_nums.contains(*x))
|
|
336
|
+
.collect();
|
|
337
|
+
nus_stmt(body, pname, &nums)
|
|
338
|
+
&& copy_descendants.iter().all(|x| lns_stmt(body, x.as_str(), &nums))
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/// analyze_aggregate's S-0 numeric-param set for one fn: the optimistic `pus_stmt` fixpoint (a param
|
|
342
|
+
/// used only as an object-field VALUE / numeric operand is numeric). Shared with the call-arg
|
|
343
|
+
/// whitelist so its candidate set covers what S-0 (via `stamp_numeric_params`) actually promotes.
|
|
344
|
+
fn s0_numeric_candidates(params: &[FunParam], body: &Statement) -> HashSet<String> {
|
|
345
|
+
let mut nums = HashSet::new();
|
|
346
|
+
collect_numeric_locals_fixpoint(body, &mut nums);
|
|
347
|
+
let mut numeric_params: HashSet<String> = params
|
|
348
|
+
.iter()
|
|
349
|
+
.filter_map(|p| match p {
|
|
350
|
+
FunParam::Simple(tp) if tp.type_ann.is_none() && tp.default.is_none() => {
|
|
351
|
+
Some(tp.name.to_string())
|
|
352
|
+
}
|
|
353
|
+
_ => None,
|
|
354
|
+
})
|
|
355
|
+
.collect();
|
|
356
|
+
loop {
|
|
357
|
+
let mut local_nums = nums.clone();
|
|
358
|
+
for n in &numeric_params {
|
|
359
|
+
local_nums.insert(n.clone());
|
|
360
|
+
}
|
|
361
|
+
let drop: Vec<String> = numeric_params
|
|
362
|
+
.iter()
|
|
363
|
+
.filter(|n| !pus_stmt(body, n.as_str(), &local_nums))
|
|
364
|
+
.cloned()
|
|
365
|
+
.collect();
|
|
366
|
+
if drop.is_empty() {
|
|
367
|
+
break;
|
|
368
|
+
}
|
|
369
|
+
for n in drop {
|
|
370
|
+
numeric_params.remove(&n);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
numeric_params
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/// Per fn (top-level and nested), the parameter POSITIONS that are numeric candidates — i.e. either
|
|
377
|
+
/// promotion path would M4-promote them to f64. The call-arg whitelist (#485/#477/#484) checks ONLY
|
|
378
|
+
/// these positions, so array/object params (e.g. native-vec devirt) never disqualify a fn.
|
|
379
|
+
pub(crate) fn numeric_candidate_positions(
|
|
380
|
+
statements: &[Statement],
|
|
381
|
+
) -> std::collections::HashMap<String, Vec<(usize, String)>> {
|
|
382
|
+
let mut out = std::collections::HashMap::new();
|
|
383
|
+
ncp_stmt_list(statements, &mut out);
|
|
384
|
+
out
|
|
385
|
+
}
|
|
386
|
+
fn ncp_stmt_list(statements: &[Statement], out: &mut std::collections::HashMap<String, Vec<(usize, String)>>) {
|
|
387
|
+
for s in statements {
|
|
388
|
+
ncp_stmt(s, out);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
fn ncp_stmt(s: &Statement, out: &mut std::collections::HashMap<String, Vec<(usize, String)>>) {
|
|
392
|
+
use Statement::*;
|
|
393
|
+
match s {
|
|
394
|
+
FunDecl { name, params, body, .. } => {
|
|
395
|
+
let mut base_nums = HashSet::new();
|
|
396
|
+
collect_numeric_locals_fixpoint(body, &mut base_nums);
|
|
397
|
+
// A position is a candidate if EITHER promotion path would take it: pi_stmt (nus_stmt) OR
|
|
398
|
+
// analyze_aggregate's S-0 (pus_stmt — which also accepts a param used as an object-field
|
|
399
|
+
// VALUE, e.g. `{ v: s }`). The whitelist must cover BOTH or a param S-0 promotes but
|
|
400
|
+
// pi_stmt doesn't (tishlang/tish#484) gets an f64 unbox with no call-arg check.
|
|
401
|
+
let s0 = s0_numeric_candidates(params, body);
|
|
402
|
+
let mut positions: Vec<(usize, String)> = Vec::new();
|
|
403
|
+
for (i, p) in params.iter().enumerate() {
|
|
404
|
+
if let FunParam::Simple(tp) = p {
|
|
405
|
+
if tp.type_ann.is_none()
|
|
406
|
+
&& tp.default.is_none()
|
|
407
|
+
&& (param_is_body_numeric_candidate(body, &base_nums, tp.name.as_ref())
|
|
408
|
+
|| s0.contains(tp.name.as_ref()))
|
|
409
|
+
{
|
|
410
|
+
positions.push((i, tp.name.to_string()));
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
if !positions.is_empty() {
|
|
415
|
+
out.insert(name.to_string(), positions);
|
|
416
|
+
}
|
|
417
|
+
ncp_stmt(body, out); // nested fns
|
|
418
|
+
}
|
|
419
|
+
Block { statements, .. } | Multi { statements, .. } => ncp_stmt_list(statements, out),
|
|
420
|
+
If { then_branch, else_branch, .. } => {
|
|
421
|
+
ncp_stmt(then_branch, out);
|
|
422
|
+
if let Some(e) = else_branch {
|
|
423
|
+
ncp_stmt(e, out);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
While { body, .. }
|
|
427
|
+
| DoWhile { body, .. }
|
|
428
|
+
| For { body, .. }
|
|
429
|
+
| ForOf { body, .. }
|
|
430
|
+
| ForIn { body, .. } => ncp_stmt(body, out),
|
|
431
|
+
Try { body, catch_body, finally_body, .. } => {
|
|
432
|
+
ncp_stmt(body, out);
|
|
433
|
+
if let Some(c) = catch_body {
|
|
434
|
+
ncp_stmt(c, out);
|
|
435
|
+
}
|
|
436
|
+
if let Some(f) = finally_body {
|
|
437
|
+
ncp_stmt(f, out);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
Switch { cases, default_body, .. } => {
|
|
441
|
+
for (_, b) in cases {
|
|
442
|
+
ncp_stmt_list(b, out);
|
|
443
|
+
}
|
|
444
|
+
if let Some(d) = default_body {
|
|
445
|
+
ncp_stmt_list(d, out);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
Export { declaration, .. } => {
|
|
449
|
+
if let tishlang_ast::ExportDeclaration::Named(inner) = declaration.as_ref() {
|
|
450
|
+
ncp_stmt(inner, out);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
_ => {}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// ---- interprocedural numeric-arg soundness (tishlang/tish#485/#477/#484) ----
|
|
458
|
+
//
|
|
459
|
+
// M4 promotes an unannotated param to native f64 and unboxes it at the (boxed) closure entry, which
|
|
460
|
+
// panics `"expected number"` if a caller ever passes a non-number. Instead of BLACKLISTING the known
|
|
461
|
+
// ways that happens (a non-numeric literal / an escaping fn / an await), compute — soundly, by
|
|
462
|
+
// construction — which parameter POSITIONS provably receive only numbers, then disqualify any fn with
|
|
463
|
+
// a body-numeric CANDIDATE position that is NOT provably numeric. An optimistic fixpoint (assume all
|
|
464
|
+
// positions numeric, then drop any that a call site violates) makes pass-through params transitive:
|
|
465
|
+
// `f(n)` where `n` is a numeric param of the caller stays numeric down the chain, back to a literal.
|
|
466
|
+
|
|
467
|
+
#[derive(Clone)]
|
|
468
|
+
enum ArgClass {
|
|
469
|
+
Num, // provably a number regardless of context (literal, arithmetic, Math)
|
|
470
|
+
NotNum, // provably NOT reliably a number (string/object/unknown-return call)
|
|
471
|
+
NeedsNumeric(Vec<String>), // a number iff every listed ident is numeric in the caller's context
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
struct CallSite {
|
|
475
|
+
caller: Option<String>, // enclosing fn (None = top level)
|
|
476
|
+
callee: String,
|
|
477
|
+
args: Vec<ArgClass>,
|
|
478
|
+
has_spread: bool,
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
fn arg_number_producing_call(callee: &Expr) -> bool {
|
|
482
|
+
match callee {
|
|
483
|
+
Expr::Ident { name, .. } => name.as_ref() == "Number",
|
|
484
|
+
Expr::Member { object, prop: tishlang_ast::MemberProp::Name { name: m, .. }, .. } => {
|
|
485
|
+
matches!(object.as_ref(), Expr::Ident { name, .. } if name.as_ref() == "Math")
|
|
486
|
+
&& matches!(
|
|
487
|
+
m.as_ref(),
|
|
488
|
+
"sqrt" | "sin" | "cos" | "tan" | "abs" | "floor" | "ceil" | "exp" | "trunc"
|
|
489
|
+
| "log" | "log2" | "log10" | "round" | "pow" | "min" | "max" | "sign"
|
|
490
|
+
| "cbrt" | "atan" | "atan2" | "asin" | "acos" | "hypot" | "random"
|
|
491
|
+
)
|
|
492
|
+
}
|
|
493
|
+
_ => false,
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
fn combine_add(l: ArgClass, r: ArgClass) -> ArgClass {
|
|
498
|
+
match (l, r) {
|
|
499
|
+
(ArgClass::NotNum, _) | (_, ArgClass::NotNum) => ArgClass::NotNum,
|
|
500
|
+
(ArgClass::Num, ArgClass::Num) => ArgClass::Num,
|
|
501
|
+
(ArgClass::Num, x) | (x, ArgClass::Num) => x,
|
|
502
|
+
(ArgClass::NeedsNumeric(mut a), ArgClass::NeedsNumeric(b)) => {
|
|
503
|
+
a.extend(b);
|
|
504
|
+
ArgClass::NeedsNumeric(a)
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
fn classify_arg(e: &Expr) -> ArgClass {
|
|
510
|
+
use tishlang_ast::BinOp::*;
|
|
511
|
+
match e {
|
|
512
|
+
Expr::Literal { value: Literal::Number(_), .. } => ArgClass::Num,
|
|
513
|
+
Expr::Ident { name, .. } => ArgClass::NeedsNumeric(vec![name.to_string()]),
|
|
514
|
+
// `-`,`*`,`/`,`%`,`**`, bitwise/shift JS-ToNumber unconditionally → always a number.
|
|
515
|
+
Expr::Binary {
|
|
516
|
+
op: Sub | Mul | Div | Mod | Pow | BitAnd | BitOr | BitXor | Shl | Shr | UShr,
|
|
517
|
+
..
|
|
518
|
+
} => ArgClass::Num,
|
|
519
|
+
// `+` is a number only if BOTH sides are (else string concat).
|
|
520
|
+
Expr::Binary { op: Add, left, right, .. } => {
|
|
521
|
+
combine_add(classify_arg(left), classify_arg(right))
|
|
522
|
+
}
|
|
523
|
+
Expr::Unary {
|
|
524
|
+
op:
|
|
525
|
+
tishlang_ast::UnaryOp::Neg
|
|
526
|
+
| tishlang_ast::UnaryOp::Pos
|
|
527
|
+
| tishlang_ast::UnaryOp::BitNot,
|
|
528
|
+
..
|
|
529
|
+
} => ArgClass::Num,
|
|
530
|
+
Expr::Call { callee, .. } | Expr::New { callee, .. } => {
|
|
531
|
+
if arg_number_producing_call(callee) {
|
|
532
|
+
ArgClass::Num
|
|
533
|
+
} else {
|
|
534
|
+
ArgClass::NotNum
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
_ => ArgClass::NotNum,
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
fn arg_ok(ac: &ArgClass, ctx: &HashSet<String>) -> bool {
|
|
542
|
+
match ac {
|
|
543
|
+
ArgClass::Num => true,
|
|
544
|
+
ArgClass::NotNum => false,
|
|
545
|
+
ArgClass::NeedsNumeric(idents) => idents.iter().all(|n| ctx.contains(n)),
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
// Collect (param-name-list, numeric-locals) for every fn (top-level and nested).
|
|
550
|
+
fn cfm_stmt(
|
|
551
|
+
s: &Statement,
|
|
552
|
+
params: &mut std::collections::HashMap<String, Vec<String>>,
|
|
553
|
+
locals: &mut std::collections::HashMap<String, HashSet<String>>,
|
|
554
|
+
) {
|
|
555
|
+
use Statement::*;
|
|
556
|
+
match s {
|
|
557
|
+
FunDecl { name, params: ps, body, .. } => {
|
|
558
|
+
let pnames: Vec<String> = ps
|
|
559
|
+
.iter()
|
|
560
|
+
.map(|p| match p {
|
|
561
|
+
FunParam::Simple(tp) => tp.name.to_string(),
|
|
562
|
+
_ => String::new(),
|
|
563
|
+
})
|
|
564
|
+
.collect();
|
|
565
|
+
params.insert(name.to_string(), pnames);
|
|
566
|
+
let mut nums = HashSet::new();
|
|
567
|
+
collect_numeric_locals_fixpoint(body, &mut nums);
|
|
568
|
+
locals.insert(name.to_string(), nums);
|
|
569
|
+
cfm_stmt(body, params, locals);
|
|
570
|
+
}
|
|
571
|
+
Block { statements, .. } | Multi { statements, .. } => {
|
|
572
|
+
for x in statements {
|
|
573
|
+
cfm_stmt(x, params, locals);
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
If { then_branch, else_branch, .. } => {
|
|
577
|
+
cfm_stmt(then_branch, params, locals);
|
|
578
|
+
if let Some(e) = else_branch {
|
|
579
|
+
cfm_stmt(e, params, locals);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
While { body, .. } | DoWhile { body, .. } | For { body, .. } | ForOf { body, .. }
|
|
583
|
+
| ForIn { body, .. } => cfm_stmt(body, params, locals),
|
|
584
|
+
Try { body, catch_body, finally_body, .. } => {
|
|
585
|
+
cfm_stmt(body, params, locals);
|
|
586
|
+
if let Some(c) = catch_body {
|
|
587
|
+
cfm_stmt(c, params, locals);
|
|
588
|
+
}
|
|
589
|
+
if let Some(f) = finally_body {
|
|
590
|
+
cfm_stmt(f, params, locals);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
Switch { cases, default_body, .. } => {
|
|
594
|
+
for (_, b) in cases {
|
|
595
|
+
for x in b {
|
|
596
|
+
cfm_stmt(x, params, locals);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
if let Some(d) = default_body {
|
|
600
|
+
for x in d {
|
|
601
|
+
cfm_stmt(x, params, locals);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
Export { declaration, .. } => {
|
|
606
|
+
if let tishlang_ast::ExportDeclaration::Named(inner) = declaration.as_ref() {
|
|
607
|
+
cfm_stmt(inner, params, locals);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
_ => {}
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Record every direct `f(...)` call in `e`, tagged with the enclosing fn; recurse into sub-exprs.
|
|
615
|
+
fn collect_calls_expr(e: &Expr, encl: &Option<String>, out: &mut Vec<CallSite>) {
|
|
616
|
+
if let Expr::Call { callee, args, .. } | Expr::New { callee, args, .. } = e {
|
|
617
|
+
if let Expr::Ident { name, .. } = callee.as_ref() {
|
|
618
|
+
let mut classes = Vec::new();
|
|
619
|
+
let mut has_spread = false;
|
|
620
|
+
for a in args {
|
|
621
|
+
match a {
|
|
622
|
+
CallArg::Expr(x) => classes.push(classify_arg(x)),
|
|
623
|
+
CallArg::Spread(_) => has_spread = true,
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
out.push(CallSite {
|
|
627
|
+
caller: encl.clone(),
|
|
628
|
+
callee: name.to_string(),
|
|
629
|
+
args: classes,
|
|
630
|
+
has_spread,
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
walk_exprs_expr(e, &mut |sub| {
|
|
635
|
+
if let Expr::Call { callee, args, .. } | Expr::New { callee, args, .. } = sub {
|
|
636
|
+
if !std::ptr::eq(sub, e) {
|
|
637
|
+
if let Expr::Ident { name, .. } = callee.as_ref() {
|
|
638
|
+
let mut classes = Vec::new();
|
|
639
|
+
let mut has_spread = false;
|
|
640
|
+
for a in args {
|
|
641
|
+
match a {
|
|
642
|
+
CallArg::Expr(x) => classes.push(classify_arg(x)),
|
|
643
|
+
CallArg::Spread(_) => has_spread = true,
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
out.push(CallSite {
|
|
647
|
+
caller: encl.clone(),
|
|
648
|
+
callee: name.to_string(),
|
|
649
|
+
args: classes,
|
|
650
|
+
has_spread,
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
fn collect_calls_stmt(s: &Statement, encl: &Option<String>, out: &mut Vec<CallSite>) {
|
|
659
|
+
use Statement::*;
|
|
660
|
+
match s {
|
|
661
|
+
FunDecl { name, body, .. } => {
|
|
662
|
+
let inner = Some(name.to_string());
|
|
663
|
+
collect_calls_stmt(body, &inner, out);
|
|
664
|
+
}
|
|
665
|
+
Block { statements, .. } | Multi { statements, .. } => {
|
|
666
|
+
for x in statements {
|
|
667
|
+
collect_calls_stmt(x, encl, out);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
VarDecl { init: Some(e), .. } => collect_calls_expr(e, encl, out),
|
|
671
|
+
VarDeclDestructure { init, .. } => collect_calls_expr(init, encl, out),
|
|
672
|
+
ExprStmt { expr, .. } => collect_calls_expr(expr, encl, out),
|
|
673
|
+
Return { value: Some(e), .. } | Throw { value: e, .. } => collect_calls_expr(e, encl, out),
|
|
674
|
+
If { cond, then_branch, else_branch, .. } => {
|
|
675
|
+
collect_calls_expr(cond, encl, out);
|
|
676
|
+
collect_calls_stmt(then_branch, encl, out);
|
|
677
|
+
if let Some(e) = else_branch {
|
|
678
|
+
collect_calls_stmt(e, encl, out);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
While { cond, body, .. } | DoWhile { cond, body, .. } => {
|
|
682
|
+
collect_calls_expr(cond, encl, out);
|
|
683
|
+
collect_calls_stmt(body, encl, out);
|
|
684
|
+
}
|
|
685
|
+
For { init, cond, update, body, .. } => {
|
|
686
|
+
if let Some(i) = init {
|
|
687
|
+
collect_calls_stmt(i, encl, out);
|
|
688
|
+
}
|
|
689
|
+
if let Some(c) = cond {
|
|
690
|
+
collect_calls_expr(c, encl, out);
|
|
691
|
+
}
|
|
692
|
+
if let Some(u) = update {
|
|
693
|
+
collect_calls_expr(u, encl, out);
|
|
694
|
+
}
|
|
695
|
+
collect_calls_stmt(body, encl, out);
|
|
696
|
+
}
|
|
697
|
+
ForOf { iterable, body, .. } | ForIn { object: iterable, body, .. } => {
|
|
698
|
+
collect_calls_expr(iterable, encl, out);
|
|
699
|
+
collect_calls_stmt(body, encl, out);
|
|
700
|
+
}
|
|
701
|
+
Switch { expr, cases, default_body, .. } => {
|
|
702
|
+
collect_calls_expr(expr, encl, out);
|
|
703
|
+
for (g, b) in cases {
|
|
704
|
+
if let Some(ge) = g {
|
|
705
|
+
collect_calls_expr(ge, encl, out);
|
|
706
|
+
}
|
|
707
|
+
for x in b {
|
|
708
|
+
collect_calls_stmt(x, encl, out);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
if let Some(d) = default_body {
|
|
712
|
+
for x in d {
|
|
713
|
+
collect_calls_stmt(x, encl, out);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
Try { body, catch_body, finally_body, .. } => {
|
|
718
|
+
collect_calls_stmt(body, encl, out);
|
|
719
|
+
if let Some(c) = catch_body {
|
|
720
|
+
collect_calls_stmt(c, encl, out);
|
|
721
|
+
}
|
|
722
|
+
if let Some(f) = finally_body {
|
|
723
|
+
collect_calls_stmt(f, encl, out);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
Export { declaration, .. } => {
|
|
727
|
+
if let tishlang_ast::ExportDeclaration::Named(inner) = declaration.as_ref() {
|
|
728
|
+
collect_calls_stmt(inner, encl, out);
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
_ => {}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/// SOUND replacement for the "nonnumeric arg" blacklist (tishlang/tish#485/#477/#484): a per-position
|
|
736
|
+
/// interprocedural fixpoint. Returns the set of fns that have a body-numeric CANDIDATE param position
|
|
737
|
+
/// which is NOT provably fed numbers at every call site — those must stay boxed (not `: number`).
|
|
738
|
+
pub(crate) fn fns_with_unsafe_numeric_arg(statements: &[Statement]) -> HashSet<String> {
|
|
739
|
+
let cand = numeric_candidate_positions(statements);
|
|
740
|
+
if cand.is_empty() {
|
|
741
|
+
return HashSet::new();
|
|
742
|
+
}
|
|
743
|
+
let mut all_params: std::collections::HashMap<String, Vec<String>> =
|
|
744
|
+
std::collections::HashMap::new();
|
|
745
|
+
let mut fn_locals: std::collections::HashMap<String, HashSet<String>> =
|
|
746
|
+
std::collections::HashMap::new();
|
|
747
|
+
for s in statements {
|
|
748
|
+
cfm_stmt(s, &mut all_params, &mut fn_locals);
|
|
749
|
+
}
|
|
750
|
+
// Top-level numeric locals — run a fixpoint ACROSS statements so a chain propagates
|
|
751
|
+
// (`let PI = …; let SOLAR_MASS = 4 * PI * PI` ⇒ both numeric, so `body(…, SOLAR_MASS)` is safe).
|
|
752
|
+
let mut top_nums = HashSet::new();
|
|
753
|
+
loop {
|
|
754
|
+
let before = top_nums.len();
|
|
755
|
+
for s in statements {
|
|
756
|
+
collect_numeric_locals_fixpoint(s, &mut top_nums);
|
|
757
|
+
}
|
|
758
|
+
if top_nums.len() == before {
|
|
759
|
+
break;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
let mut calls = Vec::new();
|
|
763
|
+
for s in statements {
|
|
764
|
+
collect_calls_stmt(s, &None, &mut calls);
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// numeric_pos[f] = positions currently believed to receive only numbers. Start optimistic (all).
|
|
768
|
+
let mut numeric_pos: std::collections::HashMap<String, HashSet<usize>> = all_params
|
|
769
|
+
.iter()
|
|
770
|
+
.map(|(f, ps)| (f.clone(), (0..ps.len()).collect()))
|
|
771
|
+
.collect();
|
|
772
|
+
|
|
773
|
+
loop {
|
|
774
|
+
let mut changed = false;
|
|
775
|
+
for cs in &calls {
|
|
776
|
+
if !numeric_pos.contains_key(&cs.callee) {
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
// Caller context: its numeric locals + params whose position is still numeric.
|
|
780
|
+
let ctx: HashSet<String> = match &cs.caller {
|
|
781
|
+
None => top_nums.clone(),
|
|
782
|
+
Some(c) => {
|
|
783
|
+
// Top-level numeric globals (`SOLAR_MASS`, `PI`, …) are numeric inside every fn,
|
|
784
|
+
// plus this fn's numeric locals + its still-numeric params.
|
|
785
|
+
let mut ctx = top_nums.clone();
|
|
786
|
+
ctx.extend(fn_locals.get(c).cloned().unwrap_or_default());
|
|
787
|
+
if let (Some(ps), Some(np)) = (all_params.get(c), numeric_pos.get(c)) {
|
|
788
|
+
for &pos in np {
|
|
789
|
+
if let Some(n) = ps.get(pos) {
|
|
790
|
+
ctx.insert(n.clone());
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
ctx
|
|
795
|
+
}
|
|
796
|
+
};
|
|
797
|
+
let arity = all_params.get(&cs.callee).map(|p| p.len()).unwrap_or(0);
|
|
798
|
+
let set = numeric_pos.get_mut(&cs.callee).unwrap();
|
|
799
|
+
if cs.has_spread {
|
|
800
|
+
if !set.is_empty() {
|
|
801
|
+
set.clear();
|
|
802
|
+
changed = true;
|
|
803
|
+
}
|
|
804
|
+
continue;
|
|
805
|
+
}
|
|
806
|
+
for pos in 0..arity {
|
|
807
|
+
if !set.contains(&pos) {
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
810
|
+
let ok = match cs.args.get(pos) {
|
|
811
|
+
Some(ac) => arg_ok(ac, &ctx),
|
|
812
|
+
None => false, // missing arg → undefined/Null → not a number
|
|
813
|
+
};
|
|
814
|
+
if !ok {
|
|
815
|
+
set.remove(&pos);
|
|
816
|
+
changed = true;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
if !changed {
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
let mut disq = HashSet::new();
|
|
826
|
+
for (f, positions) in &cand {
|
|
827
|
+
match numeric_pos.get(f) {
|
|
828
|
+
Some(np) => {
|
|
829
|
+
if positions.iter().any(|(pos, _)| !np.contains(pos)) {
|
|
830
|
+
disq.insert(f.clone());
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
None => {
|
|
834
|
+
disq.insert(f.clone());
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
disq
|
|
839
|
+
}
|
|
840
|
+
|
|
325
841
|
fn pi_stmt(s: Statement, nonnumeric: &HashSet<String>, escaping: &HashSet<String>) -> Statement {
|
|
326
842
|
if let Statement::FunDecl {
|
|
327
843
|
async_,
|
|
@@ -374,21 +890,9 @@ fn pi_stmt(s: Statement, nonnumeric: &HashSet<String>, escaping: &HashSet<String
|
|
|
374
890
|
// Monotone, locals/param-candidate-only.
|
|
375
891
|
if tp.type_ann.is_none()
|
|
376
892
|
&& tp.default.is_none()
|
|
893
|
+
&& param_is_body_numeric_candidate(&body, &base_nums, tp.name.as_ref())
|
|
377
894
|
{
|
|
378
|
-
|
|
379
|
-
nums.insert(tp.name.to_string());
|
|
380
|
-
collect_numeric_locals_fixpoint(&body, &mut nums);
|
|
381
|
-
let copy_descendants: Vec<&String> = nums
|
|
382
|
-
.iter()
|
|
383
|
-
.filter(|x| x.as_str() != tp.name.as_ref() && !base_nums.contains(*x))
|
|
384
|
-
.collect();
|
|
385
|
-
if nus_stmt(&body, tp.name.as_ref(), &nums)
|
|
386
|
-
&& copy_descendants
|
|
387
|
-
.iter()
|
|
388
|
-
.all(|x| lns_stmt(&body, x.as_str(), &nums))
|
|
389
|
-
{
|
|
390
|
-
tp.type_ann = Some(TypeAnnotation::Simple(std::sync::Arc::from("number"), tishlang_ast::Span::default()));
|
|
391
|
-
}
|
|
895
|
+
tp.type_ann = Some(TypeAnnotation::Simple(std::sync::Arc::from("number"), tishlang_ast::Span::default()));
|
|
392
896
|
}
|
|
393
897
|
FunParam::Simple(tp)
|
|
394
898
|
}
|
|
@@ -484,7 +988,7 @@ fn collect_numeric_locals(s: &Statement, out: &mut HashSet<String>) {
|
|
|
484
988
|
/// names (a copy of an already-numeric source), so it converges in at most one pass per copy-chain
|
|
485
989
|
/// link; the `out.len()` watermark detects quiescence. Soundness: every added name is a copy of a
|
|
486
990
|
/// value already proven/assumed numeric, the same basis as the literal seeding.
|
|
487
|
-
fn collect_numeric_locals_fixpoint(body: &Statement, out: &mut HashSet<String>) {
|
|
991
|
+
pub(crate) fn collect_numeric_locals_fixpoint(body: &Statement, out: &mut HashSet<String>) {
|
|
488
992
|
loop {
|
|
489
993
|
let before = out.len();
|
|
490
994
|
collect_numeric_locals(body, out);
|
|
@@ -3710,7 +4214,7 @@ pub fn analyze_aggregate(program: &Program) -> AggregateAnalysis {
|
|
|
3710
4214
|
// params either — `fn makeRecord(id, name){ return {id, name} }` called `makeRecord(1, "Alice")`
|
|
3711
4215
|
// would otherwise emit a `match … panic!("expected number")` coercion on the String. Same set the
|
|
3712
4216
|
// M4 `param_infer` path uses (infer.rs:273).
|
|
3713
|
-
let nonnumeric =
|
|
4217
|
+
let nonnumeric = fns_with_unsafe_numeric_arg(&program.statements);
|
|
3714
4218
|
// Soundness (#477): a fn referenced as a first-class VALUE (a `serve` handler / stored callback)
|
|
3715
4219
|
// is invoked via the boxed `Value::Function` path with arbitrary runtime Values, so — exactly as
|
|
3716
4220
|
// in `param_infer_program` — its params must NOT get an S-0 `: number` shadow either. Without this
|
|
@@ -390,13 +390,18 @@ impl RustType {
|
|
|
390
390
|
let field_assigns = fields
|
|
391
391
|
.iter()
|
|
392
392
|
.map(|(k, ty)| {
|
|
393
|
-
let fetch = format!("tishlang_runtime::get_prop(
|
|
393
|
+
let fetch = format!("tishlang_runtime::get_prop(_src, {:?})", k.as_ref());
|
|
394
394
|
format!("{}: {}", field_ident(k), ty.from_value_expr(&fetch))
|
|
395
395
|
})
|
|
396
396
|
.collect::<Vec<_>>()
|
|
397
397
|
.join(", ");
|
|
398
|
+
// Bind `_src` as a BORROW (not a move) of the source Value: field fetches only need
|
|
399
|
+
// `&Value` (get_prop borrows), and moving would use-after-move a caller-owned temp —
|
|
400
|
+
// e.g. an assignment-as-expression `{ let _v = obj; lhs = <coerce _v>; _v }` reuses
|
|
401
|
+
// `_v` for its value (tishlang/tish#486). `&(expr)` evaluates the source once and
|
|
402
|
+
// (for a temporary) lifetime-extends it to this block, so it's still single-eval.
|
|
398
403
|
format!(
|
|
399
|
-
"{{ let _src = {}; {} {{ {} }} }}",
|
|
404
|
+
"{{ let _src = &({}); {} {{ {} }} }}",
|
|
400
405
|
value_expr,
|
|
401
406
|
named_struct_ident(name),
|
|
402
407
|
field_assigns
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
//! tishlang/tish#484 — a string param used only as an object-field VALUE (`{ v: s }`) must not be
|
|
2
|
+
//! native-promoted to f64. analyze_aggregate's S-0 (`pus_stmt`) treats a field-value param as
|
|
3
|
+
//! numeric; the call-arg whitelist must cover S-0's candidate set, so `f("hello")` disqualifies f.
|
|
4
|
+
use std::path::PathBuf;
|
|
5
|
+
use tishlang_compile::compile_project_full;
|
|
6
|
+
fn compile(rel: &str) -> String {
|
|
7
|
+
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
8
|
+
let path = manifest.join("../..").join(rel).canonicalize().unwrap();
|
|
9
|
+
compile_project_full(&path, path.parent(), &[], true).unwrap().0
|
|
10
|
+
}
|
|
11
|
+
#[test]
|
|
12
|
+
fn obj_field_string_param_stays_boxed() {
|
|
13
|
+
for f in ["tests/regression/obj_field_string_param.tish", "tests/regression/async_param_across_await.tish"] {
|
|
14
|
+
let rust = compile(f);
|
|
15
|
+
let bad: Vec<&str> = rust.lines().filter(|l| l.contains("expected number")).collect();
|
|
16
|
+
assert!(bad.is_empty(), "{f}: string field-value param must not f64-unbox:\n{}", bad.join("\n"));
|
|
17
|
+
}
|
|
18
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
//! tishlang/tish#486 — a typed nullable-struct local reassigned to a struct literal in a conditional
|
|
2
|
+
//! must not emit a Value→struct coercion that MOVES the source temp, because the assignment-as-
|
|
3
|
+
//! expression wrapper (`{ let _v = obj; lhs = <coerce _v>; _v }`) reuses that temp for its value —
|
|
4
|
+
//! moving it produced `error[E0382]: use of moved value`. The coercion must borrow the source.
|
|
5
|
+
use std::path::PathBuf;
|
|
6
|
+
use tishlang_compile::compile_project_full;
|
|
7
|
+
|
|
8
|
+
fn compile(rel: &str) -> String {
|
|
9
|
+
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
10
|
+
let path = manifest.join("../..").join(rel).canonicalize().unwrap();
|
|
11
|
+
compile_project_full(&path, path.parent(), &[], true).unwrap().0
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
#[test]
|
|
15
|
+
fn typed_struct_coercion_borrows_source() {
|
|
16
|
+
let rust = compile("tests/regression/typed_struct_cond_assign.tish");
|
|
17
|
+
// The Value→struct coercion must bind `_src` by BORROW, never by moving a bare temp.
|
|
18
|
+
assert!(
|
|
19
|
+
rust.contains("let _src = &("),
|
|
20
|
+
"expected the struct coercion to borrow its source (`let _src = &(...)`)"
|
|
21
|
+
);
|
|
22
|
+
assert!(
|
|
23
|
+
!rust.contains("let _src = _v;"),
|
|
24
|
+
"tishlang/tish#486: struct coercion must not MOVE the wrapper temp `_v` (use-after-move / E0382)"
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
//! tishlang/tish#485/#477/#484 — SYSTEMIC: a param is native-promoted to f64 only if the
|
|
2
|
+
//! interprocedural per-position fixpoint proves every call site feeds it numbers. #485's
|
|
3
|
+
//! return-either-param helper (called with string|null results) must NOT get an f64 unbox; the
|
|
4
|
+
//! recursive + pass-through numeric fns MUST still promote (no perf regression).
|
|
5
|
+
use std::path::PathBuf;
|
|
6
|
+
use tishlang_compile::compile_project_full;
|
|
7
|
+
|
|
8
|
+
fn compile(rel: &str) -> String {
|
|
9
|
+
let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
10
|
+
let path = manifest.join("../..").join(rel).canonicalize().unwrap();
|
|
11
|
+
compile_project_full(&path, path.parent(), &[], true).unwrap().0
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
#[test]
|
|
15
|
+
fn unsafe_numeric_arg_stays_boxed_no_f64_unbox() {
|
|
16
|
+
let rust = compile("tests/regression/f64_return_either_param.tish");
|
|
17
|
+
let bad: Vec<&str> = rust.lines().filter(|l| l.contains("expected number")).collect();
|
|
18
|
+
assert!(bad.is_empty(), "no fn here does arithmetic; nothing should f64-unbox:\n{}", bad.join("\n"));
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#[test]
|
|
22
|
+
fn recursive_and_passthrough_numeric_fns_still_promote() {
|
|
23
|
+
let rust = compile("tests/regression/numeric_arg_whitelist.tish");
|
|
24
|
+
// fib(n-1)/fib(n-2): n is arithmetic → promoted; work(n) via driver's param n → pass-through.
|
|
25
|
+
assert!(
|
|
26
|
+
rust.contains("fib") && (rust.contains(": f64") || rust.contains("Value::Number")),
|
|
27
|
+
"recursive/pass-through numeric params must still get the native f64 fast path"
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -79,7 +79,7 @@ ws = [
|
|
|
79
79
|
tishlang_core = { path = "../tish_core", version = ">=0.1" }
|
|
80
80
|
tishlang_builtins = { path = "../tish_builtins", version = ">=0.1" }
|
|
81
81
|
tokio = { version = "1.50.0", features = ["rt-multi-thread", "macros", "time", "sync"], optional = true }
|
|
82
|
-
reqwest = { version = "0.13.2", default-features = false, features = ["rustls", "json", "stream"], optional = true }
|
|
82
|
+
reqwest = { version = "0.13.2", default-features = false, features = ["rustls", "json", "stream", "multipart"], optional = true }
|
|
83
83
|
futures = { version = "0.3.32", optional = true }
|
|
84
84
|
bytes = { version = "1", optional = true }
|
|
85
85
|
tiny_http = { version = "0.12.0", optional = true }
|
|
@@ -111,6 +111,57 @@ pub(crate) fn extract_body(options: Option<&Value>) -> Option<String> {
|
|
|
111
111
|
})
|
|
112
112
|
}
|
|
113
113
|
|
|
114
|
+
/// A single `multipart/form-data` part extracted from a `fetch` options object's `multipart` array.
|
|
115
|
+
pub(crate) enum MultipartPart {
|
|
116
|
+
/// A text field: `{ name, value }`.
|
|
117
|
+
Text { name: String, value: String },
|
|
118
|
+
/// A file field read from disk (binary-safe): `{ name, path, filename?, contentType? }`.
|
|
119
|
+
File {
|
|
120
|
+
name: String,
|
|
121
|
+
path: String,
|
|
122
|
+
filename: Option<String>,
|
|
123
|
+
content_type: Option<String>,
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/// Extract the `multipart` option — an array of part descriptors — from a `fetch` options object.
|
|
128
|
+
/// Each part is `{ name, value }` (text) or `{ name, path, filename?, contentType? }` (file, read as
|
|
129
|
+
/// bytes so binary uploads work). Returns `None` when there is no usable `multipart` array, so callers
|
|
130
|
+
/// fall back to `body`. A part missing `name`, or with neither `path` nor `value`, is skipped.
|
|
131
|
+
pub(crate) fn extract_multipart(options: Option<&Value>) -> Option<Vec<MultipartPart>> {
|
|
132
|
+
let field = options.and_then(|v| match v {
|
|
133
|
+
Value::Object(obj) => obj.borrow().strings.get("multipart").cloned(),
|
|
134
|
+
_ => None,
|
|
135
|
+
})?;
|
|
136
|
+
let items = match field {
|
|
137
|
+
Value::Array(a) => a.borrow().clone(),
|
|
138
|
+
_ => return None,
|
|
139
|
+
};
|
|
140
|
+
let mut parts = Vec::with_capacity(items.len());
|
|
141
|
+
for item in items {
|
|
142
|
+
let obj = match &item {
|
|
143
|
+
Value::Object(o) => o.borrow(),
|
|
144
|
+
_ => continue,
|
|
145
|
+
};
|
|
146
|
+
let name = match obj.strings.get("name") {
|
|
147
|
+
Some(v) => v.to_display_string(),
|
|
148
|
+
None => continue,
|
|
149
|
+
};
|
|
150
|
+
let filename = obj.strings.get("filename").map(|v| v.to_display_string());
|
|
151
|
+
let content_type = obj.strings.get("contentType").map(|v| v.to_display_string());
|
|
152
|
+
if let Some(path) = obj.strings.get("path").map(|v| v.to_display_string()) {
|
|
153
|
+
parts.push(MultipartPart::File { name, path, filename, content_type });
|
|
154
|
+
} else if let Some(value) = obj.strings.get("value").map(|v| v.to_display_string()) {
|
|
155
|
+
parts.push(MultipartPart::Text { name, value });
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if parts.is_empty() {
|
|
159
|
+
None
|
|
160
|
+
} else {
|
|
161
|
+
Some(parts)
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
114
165
|
pub(crate) fn build_error_response(error: &str) -> Value {
|
|
115
166
|
let mut obj: ObjectMap = ObjectMap::with_capacity(2);
|
|
116
167
|
obj.insert(Arc::from("error"), Value::String(error.into()));
|
|
@@ -10,7 +10,10 @@ use futures::Stream;
|
|
|
10
10
|
use futures::StreamExt;
|
|
11
11
|
use tishlang_core::{NativeFn, ObjectMap, TishOpaque, TishPromise, Value};
|
|
12
12
|
|
|
13
|
-
use crate::http::{
|
|
13
|
+
use crate::http::{
|
|
14
|
+
build_error_response, extract_body, extract_headers, extract_method, extract_multipart,
|
|
15
|
+
MultipartPart,
|
|
16
|
+
};
|
|
14
17
|
|
|
15
18
|
// --- Promises (Send payloads only; Value built on awaiting thread) ---
|
|
16
19
|
|
|
@@ -479,6 +482,7 @@ async fn send_request_parts(
|
|
|
479
482
|
method: String,
|
|
480
483
|
headers: Vec<(String, String)>,
|
|
481
484
|
body: Option<String>,
|
|
485
|
+
multipart: Option<Vec<MultipartPart>>,
|
|
482
486
|
) -> Result<reqwest::Response, String> {
|
|
483
487
|
if fetch_block_private() {
|
|
484
488
|
ssrf_preflight(&url).await?;
|
|
@@ -496,7 +500,39 @@ async fn send_request_parts(
|
|
|
496
500
|
for (key, value) in headers {
|
|
497
501
|
req = req.header(key, value);
|
|
498
502
|
}
|
|
499
|
-
|
|
503
|
+
// A `multipart` option takes precedence over `body` — it builds a multipart/form-data request
|
|
504
|
+
// (reqwest sets the Content-Type + boundary). File parts are read as bytes, so binary uploads work.
|
|
505
|
+
if let Some(parts) = multipart {
|
|
506
|
+
let mut form = reqwest::multipart::Form::new();
|
|
507
|
+
for part in parts {
|
|
508
|
+
match part {
|
|
509
|
+
MultipartPart::Text { name, value } => {
|
|
510
|
+
form = form.text(name, value);
|
|
511
|
+
}
|
|
512
|
+
MultipartPart::File {
|
|
513
|
+
name,
|
|
514
|
+
path,
|
|
515
|
+
filename,
|
|
516
|
+
content_type,
|
|
517
|
+
} => {
|
|
518
|
+
let bytes = std::fs::read(&path)
|
|
519
|
+
.map_err(|e| format!("multipart file '{path}': {e}"))?;
|
|
520
|
+
let fname = filename.unwrap_or_else(|| {
|
|
521
|
+
std::path::Path::new(&path)
|
|
522
|
+
.file_name()
|
|
523
|
+
.map(|s| s.to_string_lossy().into_owned())
|
|
524
|
+
.unwrap_or_else(|| name.clone())
|
|
525
|
+
});
|
|
526
|
+
let mut p = reqwest::multipart::Part::bytes(bytes).file_name(fname);
|
|
527
|
+
if let Some(ct) = content_type {
|
|
528
|
+
p = p.mime_str(&ct).map_err(|e| e.to_string())?;
|
|
529
|
+
}
|
|
530
|
+
form = form.part(name, p);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
req = req.multipart(form);
|
|
535
|
+
} else if let Some(body) = body {
|
|
500
536
|
req = req.body(body);
|
|
501
537
|
}
|
|
502
538
|
req.send().await.map_err(|e| e.to_string())
|
|
@@ -517,10 +553,11 @@ pub fn fetch_promise_from_args(args: Vec<Value>) -> Value {
|
|
|
517
553
|
let method = extract_method(args.get(1));
|
|
518
554
|
let headers = extract_headers(args.get(1));
|
|
519
555
|
let body = extract_body(args.get(1));
|
|
556
|
+
let multipart = extract_multipart(args.get(1));
|
|
520
557
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
|
521
558
|
crate::http::RUNTIME.with(|rt| {
|
|
522
559
|
rt.spawn(async move {
|
|
523
|
-
let r = send_request_parts(url, method, headers, body).await;
|
|
560
|
+
let r = send_request_parts(url, method, headers, body, multipart).await;
|
|
524
561
|
let _ = tx.send(r);
|
|
525
562
|
});
|
|
526
563
|
});
|
|
@@ -541,7 +578,13 @@ pub fn fetch_all_promise_from_args(args: Vec<Value>) -> Value {
|
|
|
541
578
|
}
|
|
542
579
|
};
|
|
543
580
|
#[allow(clippy::type_complexity)]
|
|
544
|
-
let mut parts: Vec<(
|
|
581
|
+
let mut parts: Vec<(
|
|
582
|
+
String,
|
|
583
|
+
String,
|
|
584
|
+
Vec<(String, String)>,
|
|
585
|
+
Option<String>,
|
|
586
|
+
Option<Vec<MultipartPart>>,
|
|
587
|
+
)> = Vec::new();
|
|
545
588
|
for req in requests {
|
|
546
589
|
let (url, opt) = match &req {
|
|
547
590
|
Value::String(s) => (s.to_string(), None),
|
|
@@ -576,14 +619,15 @@ pub fn fetch_all_promise_from_args(args: Vec<Value>) -> Value {
|
|
|
576
619
|
let method = extract_method(opt.as_ref());
|
|
577
620
|
let headers = extract_headers(opt.as_ref());
|
|
578
621
|
let body = extract_body(opt.as_ref());
|
|
579
|
-
|
|
622
|
+
let multipart = extract_multipart(opt.as_ref());
|
|
623
|
+
parts.push((url, method, headers, body, multipart));
|
|
580
624
|
}
|
|
581
625
|
let (tx, rx) = tokio::sync::oneshot::channel();
|
|
582
626
|
crate::http::RUNTIME.with(|rt| {
|
|
583
627
|
rt.spawn(async move {
|
|
584
628
|
let futs: Vec<_> = parts
|
|
585
629
|
.into_iter()
|
|
586
|
-
.map(|(url, m, h, b)| send_request_parts(url, m, h, b))
|
|
630
|
+
.map(|(url, m, h, b, mp)| send_request_parts(url, m, h, b, mp))
|
|
587
631
|
.collect();
|
|
588
632
|
let results = futures::future::join_all(futs).await;
|
|
589
633
|
let mapped: Vec<Result<reqwest::Response, String>> = results.into_iter().collect();
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|