• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

davidcole1340 / ext-php-rs / 16719453544

04 Aug 2025 09:33AM UTC coverage: 27.104% (-0.04%) from 27.144%
16719453544

Pull #542

github

web-flow
Merge 8308d2668 into 34caede39
Pull Request #542: feat: Add constructor visability

4 of 21 new or added lines in 4 files covered. (19.05%)

86 existing lines in 4 files now uncovered.

1127 of 4158 relevant lines covered (27.1%)

5.66 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/crates/macros/src/function.rs
1
use std::collections::HashMap;
2

3
use darling::{FromAttributes, ToTokens};
4
use proc_macro2::{Ident, Span, TokenStream};
5
use quote::{format_ident, quote};
6
use syn::spanned::Spanned as _;
7
use syn::{Expr, FnArg, GenericArgument, ItemFn, PatType, PathArguments, Type, TypePath};
8

9
use crate::helpers::get_docs;
10
use crate::parsing::{PhpRename, RenameRule, Visibility};
11
use crate::prelude::*;
12
use crate::syn_ext::DropLifetimes;
13

14
pub fn wrap(input: &syn::Path) -> Result<TokenStream> {
×
15
    let Some(func_name) = input.get_ident() else {
×
16
        bail!(input => "Pass a PHP function name into `wrap_function!()`.");
×
17
    };
18
    let builder_func = format_ident!("_internal_{func_name}");
19

20
    Ok(quote! {{
21
        (<#builder_func as ::ext_php_rs::internal::function::PhpFunction>::FUNCTION_ENTRY)()
22
    }})
23
}
24

25
#[derive(FromAttributes, Default, Debug)]
26
#[darling(default, attributes(php), forward_attrs(doc))]
27
struct PhpFunctionAttribute {
28
    #[darling(flatten)]
29
    rename: PhpRename,
30
    defaults: HashMap<Ident, Expr>,
31
    optional: Option<Ident>,
32
    vis: Option<Visibility>,
33
    attrs: Vec<syn::Attribute>,
34
}
35

36
pub fn parser(mut input: ItemFn) -> Result<TokenStream> {
×
37
    let php_attr = PhpFunctionAttribute::from_attributes(&input.attrs)?;
×
38
    input.attrs.retain(|attr| !attr.path().is_ident("php"));
×
39

40
    let args = Args::parse_from_fnargs(input.sig.inputs.iter(), php_attr.defaults)?;
×
41
    if let Some(ReceiverArg { span, .. }) = args.receiver {
×
42
        bail!(span => "Receiver arguments are invalid on PHP functions. See `#[php_impl]`.");
43
    }
44

45
    let docs = get_docs(&php_attr.attrs)?;
×
46

47
    let func = Function::new(
48
        &input.sig,
49
        php_attr
50
            .rename
51
            .rename(input.sig.ident.to_string(), RenameRule::Snake),
52
        args,
53
        php_attr.optional,
54
        docs,
55
    );
56
    let function_impl = func.php_function_impl();
57

58
    Ok(quote! {
59
        #input
60
        #function_impl
61
    })
62
}
63

64
#[derive(Debug)]
65
pub struct Function<'a> {
66
    /// Identifier of the Rust function associated with the function.
67
    pub ident: &'a Ident,
68
    /// Name of the function in PHP.
69
    pub name: String,
70
    /// Function arguments.
71
    pub args: Args<'a>,
72
    /// Function outputs.
73
    pub output: Option<&'a Type>,
74
    /// The first optional argument of the function.
75
    pub optional: Option<Ident>,
76
    /// Doc comments for the function.
77
    pub docs: Vec<String>,
78
}
79

80
#[derive(Debug)]
81
pub enum CallType<'a> {
82
    Function,
83
    Method {
84
        class: &'a syn::Path,
85
        receiver: MethodReceiver,
86
    },
87
}
88

89
/// Type of receiver on the method.
90
#[derive(Debug)]
91
pub enum MethodReceiver {
92
    /// Static method - has no receiver.
93
    Static,
94
    /// Class method, takes `&self` or `&mut self`.
95
    Class,
96
    /// Class method, takes `&mut ZendClassObject<Self>`.
97
    ZendClassObject,
98
}
99

100
impl<'a> Function<'a> {
101
    /// Parse a function.
102
    ///
103
    /// # Parameters
104
    ///
105
    /// * `sig` - Function signature.
106
    /// * `name` - Function name in PHP land.
107
    /// * `args` - Function arguments.
108
    /// * `optional` - The ident of the first optional argument.
109
    pub fn new(
×
110
        sig: &'a syn::Signature,
111
        name: String,
112
        args: Args<'a>,
113
        optional: Option<Ident>,
114
        docs: Vec<String>,
115
    ) -> Self {
116
        Self {
117
            ident: &sig.ident,
×
118
            name,
119
            args,
120
            output: match &sig.output {
×
121
                syn::ReturnType::Default => None,
122
                syn::ReturnType::Type(_, ty) => Some(&**ty),
123
            },
124
            optional,
125
            docs,
126
        }
127
    }
128

129
    /// Generates an internal identifier for the function.
130
    pub fn internal_ident(&self) -> Ident {
×
131
        format_ident!("_internal_{}", &self.ident)
×
132
    }
133

134
    /// Generates the function builder for the function.
135
    pub fn function_builder(&self, call_type: CallType) -> TokenStream {
×
136
        let name = &self.name;
×
137
        let (required, not_required) = self.args.split_args(self.optional.as_ref());
×
138

139
        // `handler` impl
140
        let arg_declarations = self
×
141
            .args
×
142
            .typed
×
143
            .iter()
144
            .map(TypedArg::arg_declaration)
×
145
            .collect::<Vec<_>>();
146

147
        // `entry` impl
148
        let required_args = required
×
149
            .iter()
150
            .map(TypedArg::arg_builder)
×
151
            .collect::<Vec<_>>();
152
        let not_required_args = not_required
×
153
            .iter()
154
            .map(TypedArg::arg_builder)
×
155
            .collect::<Vec<_>>();
156

157
        let returns = self.build_returns();
×
158
        let result = self.build_result(call_type, required, not_required);
×
159
        let docs = if self.docs.is_empty() {
×
160
            quote! {}
×
161
        } else {
162
            let docs = &self.docs;
×
163
            quote! {
×
164
                .docs(&[#(#docs),*])
×
165
            }
166
        };
167

168
        quote! {
×
169
            ::ext_php_rs::builders::FunctionBuilder::new(#name, {
×
170
                ::ext_php_rs::zend_fastcall! {
×
171
                    extern fn handler(
×
172
                        ex: &mut ::ext_php_rs::zend::ExecuteData,
×
173
                        retval: &mut ::ext_php_rs::types::Zval,
×
174
                    ) {
175
                        use ::ext_php_rs::convert::IntoZval;
×
176

177
                        #(#arg_declarations)*
×
178
                        let result = {
×
179
                            #result
×
180
                        };
181

182
                        if let Err(e) = result.set_zval(retval, false) {
×
183
                            let e: ::ext_php_rs::exception::PhpException = e.into();
×
184
                            e.throw().expect("Failed to throw PHP exception.");
×
185
                        }
186
                    }
187
                }
188
                handler
×
189
            })
190
            #(.arg(#required_args))*
×
191
            .not_required()
×
192
            #(.arg(#not_required_args))*
×
193
            #returns
×
194
            #docs
×
195
        }
196
    }
197

198
    fn build_returns(&self) -> Option<TokenStream> {
×
199
        self.output.cloned().map(|mut output| {
×
200
            output.drop_lifetimes();
×
201
            quote! {
×
202
                .returns(
×
203
                    <#output as ::ext_php_rs::convert::IntoZval>::TYPE,
×
204
                    false,
×
205
                    <#output as ::ext_php_rs::convert::IntoZval>::NULLABLE,
×
206
                )
207
            }
208
        })
209
    }
210

211
    fn build_result(
×
212
        &self,
213
        call_type: CallType,
214
        required: &[TypedArg<'_>],
215
        not_required: &[TypedArg<'_>],
216
    ) -> TokenStream {
217
        let ident = self.ident;
×
218
        let required_arg_names: Vec<_> = required.iter().map(|arg| arg.name).collect();
×
219
        let not_required_arg_names: Vec<_> = not_required.iter().map(|arg| arg.name).collect();
×
220

221
        let arg_accessors = self.args.typed.iter().map(|arg| {
×
222
            arg.accessor(|e| {
×
223
                quote! {
×
224
                    #e.throw().expect("Failed to throw PHP exception.");
×
225
                    return;
×
226
                }
227
            })
228
        });
229

230
        match call_type {
×
231
            CallType::Function => quote! {
×
232
                let parse = ex.parser()
×
233
                    #(.arg(&mut #required_arg_names))*
×
234
                    .not_required()
×
235
                    #(.arg(&mut #not_required_arg_names))*
×
236
                    .parse();
×
237
                if parse.is_err() {
×
238
                    return;
×
239
                }
240

241
                #ident(#({#arg_accessors}),*)
×
242
            },
243
            CallType::Method { class, receiver } => {
×
244
                let this = match receiver {
×
245
                    MethodReceiver::Static => quote! {
×
246
                        let parse = ex.parser();
×
247
                    },
248
                    MethodReceiver::ZendClassObject | MethodReceiver::Class => quote! {
×
249
                        let (parse, this) = ex.parser_method::<#class>();
×
250
                        let this = match this {
×
251
                            Some(this) => this,
×
252
                            None => {
×
253
                                ::ext_php_rs::exception::PhpException::default("Failed to retrieve reference to `$this`".into())
×
254
                                    .throw()
×
255
                                    .unwrap();
×
256
                                return;
×
257
                            }
258
                        };
259
                    },
260
                };
261
                let call = match receiver {
×
262
                    MethodReceiver::Static => {
×
263
                        quote! { #class::#ident(#({#arg_accessors}),*) }
×
264
                    }
265
                    MethodReceiver::Class => quote! { this.#ident(#({#arg_accessors}),*) },
×
266
                    MethodReceiver::ZendClassObject => {
×
267
                        quote! { #class::#ident(this, #({#arg_accessors}),*) }
×
268
                    }
269
                };
270
                quote! {
×
271
                    #this
×
272
                    let parse_result = parse
×
273
                        #(.arg(&mut #required_arg_names))*
×
274
                        .not_required()
×
275
                        #(.arg(&mut #not_required_arg_names))*
×
276
                        .parse();
×
277
                    if parse_result.is_err() {
×
278
                        return;
×
279
                    }
280

281
                    #call
×
282
                }
283
            }
284
        }
285
    }
286

287
    /// Generates a struct and impl for the `PhpFunction` trait.
288
    pub fn php_function_impl(&self) -> TokenStream {
×
289
        let internal_ident = self.internal_ident();
×
290
        let builder = self.function_builder(CallType::Function);
×
291

292
        quote! {
×
293
            #[doc(hidden)]
×
294
            #[allow(non_camel_case_types)]
×
295
            struct #internal_ident;
×
296

297
            impl ::ext_php_rs::internal::function::PhpFunction for #internal_ident {
×
298
                const FUNCTION_ENTRY: fn() -> ::ext_php_rs::builders::FunctionBuilder<'static> = {
×
299
                    fn entry() -> ::ext_php_rs::builders::FunctionBuilder<'static>
×
300
                    {
301
                        #builder
×
302
                    }
303
                    entry
×
304
                };
305
            }
306
        }
307
    }
308

309
    /// Returns a constructor metadata object for this function. This doesn't
310
    /// check if the function is a constructor, however.
NEW
311
    pub fn constructor_meta(&self, class: &syn::Path, Visibility: Option<&Visibility>) -> TokenStream {
×
NEW
312
        let ident = self.ident;
×
NEW
313
        let (required, not_required) = self.args.split_args(self.optional.as_ref());
×
NEW
314
        let required_args = required
×
315
            .iter()
316
            .map(TypedArg::arg_builder)
×
317
            .collect::<Vec<_>>();
318
        let not_required_args = not_required
×
319
            .iter()
320
            .map(TypedArg::arg_builder)
×
321
            .collect::<Vec<_>>();
322

UNCOV
323
        let required_arg_names: Vec<_> = required.iter().map(|arg| arg.name).collect();
×
324
        let not_required_arg_names: Vec<_> = not_required.iter().map(|arg| arg.name).collect();
×
UNCOV
325
        let arg_declarations = self
×
UNCOV
326
            .args
×
327
            .typed
×
328
            .iter()
329
            .map(TypedArg::arg_declaration)
×
330
            .collect::<Vec<_>>();
331
        let arg_accessors = self.args.typed.iter().map(|arg| {
×
UNCOV
332
            arg.accessor(
×
333
                |e| quote! { return ::ext_php_rs::class::ConstructorResult::Exception(#e); },
×
334
            )
335
        });
336
        let variadic = self.args.typed.iter().any(|arg| arg.variadic).then(|| {
×
337
            quote! {
×
UNCOV
338
                .variadic()
×
339
            }
340
        });
341
        let docs = &self.docs;
×
342
        let flags = Visibility.option_tokens();
×
343

UNCOV
344
        quote! {
×
345
            ::ext_php_rs::class::ConstructorMeta {
×
NEW
346
                constructor: {
×
347
                    fn inner(ex: &mut ::ext_php_rs::zend::ExecuteData) -> ::ext_php_rs::class::ConstructorResult<#class> {
×
348
                        #(#arg_declarations)*
×
349
                        let parse = ex.parser()
×
350
                            #(.arg(&mut #required_arg_names))*
×
351
                            .not_required()
×
352
                            #(.arg(&mut #not_required_arg_names))*
×
353
                            #variadic
×
354
                            .parse();
×
355
                        if parse.is_err() {
×
356
                            return ::ext_php_rs::class::ConstructorResult::ArgError;
×
357
                        }
358
                        #class::#ident(#({#arg_accessors}),*).into()
×
359
                    }
360
                    inner
×
361
                },
362
                build_fn: {
×
UNCOV
363
                    fn inner(func: ::ext_php_rs::builders::FunctionBuilder) -> ::ext_php_rs::builders::FunctionBuilder {
×
364
                        func
×
UNCOV
365
                            .docs(&[#(#docs),*])
×
366
                            #(.arg(#required_args))*
×
367
                            .not_required()
×
368
                            #(.arg(#not_required_args))*
×
369
                            #variadic
×
370
                    }
371
                    inner
×
372
                },
373
                flags: #flags
×
374
            }
375
        }
376
    }
377
}
378

379
#[derive(Debug)]
380
pub struct ReceiverArg {
381
    pub _mutable: bool,
382
    pub span: Span,
383
}
384

385
#[derive(Debug)]
386
pub struct TypedArg<'a> {
387
    pub name: &'a Ident,
388
    pub ty: Type,
389
    pub nullable: bool,
390
    pub default: Option<Expr>,
391
    pub as_ref: bool,
392
    pub variadic: bool,
393
}
394

395
#[derive(Debug)]
396
pub struct Args<'a> {
397
    pub receiver: Option<ReceiverArg>,
398
    pub typed: Vec<TypedArg<'a>>,
399
}
400

401
impl<'a> Args<'a> {
UNCOV
402
    pub fn parse_from_fnargs(
×
403
        args: impl Iterator<Item = &'a FnArg>,
404
        mut defaults: HashMap<Ident, Expr>,
405
    ) -> Result<Self> {
406
        let mut result = Self {
407
            receiver: None,
UNCOV
408
            typed: vec![],
×
409
        };
UNCOV
410
        for arg in args {
×
UNCOV
411
            match arg {
×
412
                FnArg::Receiver(receiver) => {
×
UNCOV
413
                    if receiver.reference.is_none() {
×
414
                        bail!(receiver => "PHP objects are heap-allocated and cannot be passed by value. Try using `&self` or `&mut self`.");
×
415
                    } else if result.receiver.is_some() {
×
416
                        bail!(receiver => "Too many receivers specified.")
×
417
                    }
418
                    result.receiver.replace(ReceiverArg {
×
419
                        _mutable: receiver.mutability.is_some(),
×
420
                        span: receiver.span(),
×
421
                    });
422
                }
423
                FnArg::Typed(PatType { pat, ty, .. }) => {
×
424
                    let syn::Pat::Ident(syn::PatIdent { ident, .. }) = &**pat else {
×
UNCOV
425
                        bail!(pat => "Unsupported argument.");
×
426
                    };
427

428
                    // If the variable is `&[&Zval]` treat it as the variadic argument.
429
                    let default = defaults.remove(ident);
×
UNCOV
430
                    let nullable = type_is_nullable(ty.as_ref(), default.is_some())?;
×
UNCOV
431
                    let (variadic, as_ref, ty) = Self::parse_typed(ty);
×
UNCOV
432
                    result.typed.push(TypedArg {
×
433
                        name: ident,
×
434
                        ty,
×
435
                        nullable,
×
436
                        default,
×
437
                        as_ref,
×
438
                        variadic,
×
439
                    });
440
                }
441
            }
442
        }
UNCOV
443
        Ok(result)
×
444
    }
445

UNCOV
446
    fn parse_typed(ty: &Type) -> (bool, bool, Type) {
×
447
        match ty {
×
UNCOV
448
            Type::Reference(ref_) => {
×
UNCOV
449
                let as_ref = ref_.mutability.is_some();
×
450
                match ref_.elem.as_ref() {
×
451
                    Type::Slice(slice) => (
×
452
                        // TODO: Allow specifying the variadic type.
453
                        slice.elem.to_token_stream().to_string() == "& Zval",
×
454
                        as_ref,
×
455
                        ty.clone(),
×
456
                    ),
457
                    _ => (false, as_ref, ty.clone()),
×
458
                }
459
            }
UNCOV
460
            Type::Path(TypePath { path, .. }) => {
×
461
                let mut as_ref = false;
×
462

463
                // For for types that are `Option<&mut T>` to turn them into
464
                // `Option<&T>`, marking the Arg as as "passed by reference".
465
                let ty = path
×
UNCOV
466
                    .segments
×
467
                    .last()
UNCOV
468
                    .filter(|seg| seg.ident == "Option")
×
469
                    .and_then(|seg| {
×
470
                        if let PathArguments::AngleBracketed(args) = &seg.arguments {
×
UNCOV
471
                            args.args
×
472
                                .iter()
×
473
                                .find(|arg| matches!(arg, GenericArgument::Type(_)))
×
474
                                .and_then(|ga| match ga {
×
475
                                    GenericArgument::Type(ty) => Some(match ty {
×
476
                                        Type::Reference(r) => {
×
477
                                            let mut new_ref = r.clone();
×
478
                                            new_ref.mutability = None;
×
479
                                            as_ref = true;
×
480
                                            Type::Reference(new_ref)
×
481
                                        }
482
                                        _ => ty.clone(),
×
483
                                    }),
484
                                    _ => None,
×
485
                                })
486
                        } else {
UNCOV
487
                            None
×
488
                        }
489
                    })
UNCOV
490
                    .unwrap_or_else(|| ty.clone());
×
491
                (false, as_ref, ty.clone())
×
492
            }
UNCOV
493
            _ => (false, false, ty.clone()),
×
494
        }
495
    }
496

497
    /// Splits the typed arguments into two slices:
498
    ///
499
    /// 1. Required arguments.
500
    /// 2. Non-required arguments.
501
    ///
502
    /// # Parameters
503
    ///
504
    /// * `optional` - The first optional argument. If [`None`], the optional
505
    ///   arguments will be from the first nullable argument after the last
506
    ///   non-nullable argument to the end of the arguments.
UNCOV
507
    pub fn split_args(&self, optional: Option<&Ident>) -> (&[TypedArg<'a>], &[TypedArg<'a>]) {
×
UNCOV
508
        let mut mid = None;
×
UNCOV
509
        for (i, arg) in self.typed.iter().enumerate() {
×
UNCOV
510
            if let Some(optional) = optional {
×
511
                if optional == arg.name {
×
512
                    mid.replace(i);
×
513
                }
514
            } else if mid.is_none() && arg.nullable {
×
515
                mid.replace(i);
×
516
            } else if !arg.nullable {
×
UNCOV
517
                mid.take();
×
518
            }
519
        }
520
        match mid {
×
521
            Some(mid) => (&self.typed[..mid], &self.typed[mid..]),
×
UNCOV
522
            None => (&self.typed[..], &self.typed[0..0]),
×
523
        }
524
    }
525
}
526

527
impl TypedArg<'_> {
528
    /// Returns a 'clean type' with the lifetimes removed. This allows the type
529
    /// to be used outside of the original function context.
UNCOV
530
    fn clean_ty(&self) -> Type {
×
UNCOV
531
        let mut ty = self.ty.clone();
×
UNCOV
532
        ty.drop_lifetimes();
×
533

534
        // Variadic arguments are passed as slices, so we need to extract the
535
        // inner type.
536
        if self.variadic {
×
UNCOV
537
            let Type::Reference(reference) = &ty else {
×
UNCOV
538
                return ty;
×
539
            };
540

541
            if let Type::Slice(inner) = &*reference.elem {
×
542
                return *inner.elem.clone();
543
            }
544
        }
545

UNCOV
546
        ty
×
547
    }
548

549
    /// Returns a token stream containing an argument declaration, where the
550
    /// name of the variable holding the arg is the name of the argument.
UNCOV
551
    fn arg_declaration(&self) -> TokenStream {
×
UNCOV
552
        let name = self.name;
×
UNCOV
553
        let val = self.arg_builder();
×
UNCOV
554
        quote! {
×
555
            let mut #name = #val;
556
        }
557
    }
558

559
    /// Returns a token stream containing the `Arg` definition to be passed to
560
    /// `ext-php-rs`.
UNCOV
561
    fn arg_builder(&self) -> TokenStream {
×
UNCOV
562
        let name = self.name.to_string();
×
UNCOV
563
        let ty = self.clean_ty();
×
UNCOV
564
        let null = if self.nullable {
×
565
            Some(quote! { .allow_null() })
×
566
        } else {
567
            None
×
568
        };
569
        let default = self.default.as_ref().map(|val| {
×
UNCOV
570
            let val = val.to_token_stream().to_string();
×
571
            quote! {
×
572
                .default(#val)
573
            }
574
        });
575
        let as_ref = if self.as_ref {
×
UNCOV
576
            Some(quote! { .as_ref() })
×
577
        } else {
UNCOV
578
            None
×
579
        };
580
        let variadic = self.variadic.then(|| quote! { .is_variadic() });
×
UNCOV
581
        quote! {
×
582
            ::ext_php_rs::args::Arg::new(#name, <#ty as ::ext_php_rs::convert::FromZvalMut>::TYPE)
583
                #null
584
                #default
585
                #as_ref
586
                #variadic
587
        }
588
    }
589

590
    /// Get the accessor used to access the value of the argument.
UNCOV
591
    fn accessor(&self, bail_fn: impl Fn(TokenStream) -> TokenStream) -> TokenStream {
×
UNCOV
592
        let name = self.name;
×
UNCOV
593
        if let Some(default) = &self.default {
×
UNCOV
594
            quote! {
×
595
                #name.val().unwrap_or(#default.into())
×
596
            }
597
        } else if self.variadic {
×
598
            quote! {
×
599
                &#name.variadic_vals()
×
600
            }
601
        } else if self.nullable {
×
602
            // Originally I thought we could just use the below case for `null` options, as
603
            // `val()` will return `Option<Option<T>>`, however, this isn't the case when
604
            // the argument isn't given, as the underlying zval is null.
605
            quote! {
×
UNCOV
606
                #name.val()
×
607
            }
608
        } else {
609
            let bail = bail_fn(quote! {
×
610
                ::ext_php_rs::exception::PhpException::default(
×
UNCOV
611
                    concat!("Invalid value given for argument `", stringify!(#name), "`.").into()
×
612
                )
613
            });
614
            quote! {
×
615
                match #name.val() {
×
UNCOV
616
                    Some(val) => val,
×
UNCOV
617
                    None => {
×
618
                        #bail;
×
619
                    }
620
                }
621
            }
622
        }
623
    }
624
}
625

626
/// Returns true of the given type is nullable in PHP.
627
// TODO(david): Eventually move to compile-time constants for this (similar to
628
// FromZval::NULLABLE).
UNCOV
629
pub fn type_is_nullable(ty: &Type, has_default: bool) -> Result<bool> {
×
UNCOV
630
    Ok(match ty {
×
UNCOV
631
        syn::Type::Path(path) => {
×
632
            has_default
633
                || path
×
634
                    .path
×
635
                    .segments
×
UNCOV
636
                    .iter()
×
637
                    .next_back()
638
                    .is_some_and(|seg| seg.ident == "Option")
×
639
        }
640
        syn::Type::Reference(_) => false, /* Reference cannot be nullable unless */
×
641
        // wrapped in `Option` (in that case it'd be a Path).
642
        _ => bail!(ty => "Unsupported argument type."),
×
643
    })
644
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc