platify/
lib.rs

1//! # Platify
2//!
3//! **Platify** streamlines the development of cross-platform Rust applications by reducing the boilerplate
4//! associated with `#[cfg(...)]` attributes.
5//!
6//! Instead of manually cluttering your code with complex `cfg` checks and duplicate function definitions,
7//! Platify allows you to define platform-specific behavior using a clean, declarative attribute syntax.
8//!
9//! ## Features
10//!
11//! *   **`#[sys_function]`**: Automatically dispatches method calls to platform-specific implementations (e.g., `fn run()` calls `Self::run_impl()`).
12//! *   **`#[sys_trait_function]`**: Applies platform configuration to trait method definitions.
13//! *   **`#[sys_struct]`**: Generates platform-specific type aliases (e.g., `MyStruct` -> `MyStructLinux`) and optionally enforces trait bounds (e.g., `Send + Sync`) at compile time.
14//! *   **`#[platform_mod]`**: Declares platform-dependent modules backed by OS-specific files, with strict visibility control.
15//! *   **Flexible Logic**: Supports explicit inclusion (`include`) and exclusion (`exclude`) of platforms.
16//! *   **Platform Groups**: Includes helper keywords like `posix` (Linux + macOS) or `all`.
17//!
18//! ## Supported Keywords
19//!
20//! The following keywords can be used inside `include(...)` and `exclude(...)`:
21//!
22//! *   `linux`
23//! *   `macos`
24//! *   `windows`
25//! *   `posix` (Expands to: `linux`, `macos`)
26//! *   `all` (Expands to: `linux`, `macos`, `windows`)
27//!
28//! ## Logic
29//!
30//! The set of allowed platforms is calculated as follows:
31//! 1. Start with the `include` list. If `include` is omitted, it defaults to `all`.
32//! 2. Remove any platforms specified in the `exclude` list.
33//! 3. Generate the corresponding `#[cfg(any(...))]` attributes.
34//!
35//! ---
36//!
37//! ## Examples
38//!
39//! ### 1. Using `#[sys_function]`
40//!
41//! This macro generates a default method that delegates to a `_impl` suffixed method.
42//!
43//! ```rust
44//! # use platify::sys_function;
45//! struct SystemManager;
46//!
47//! impl SystemManager {
48//!     /// This method is available on ALL supported platforms (default).
49//!     /// It calls `reboot_impl` internally.
50//!     #[sys_function]
51//!     pub fn reboot(&self) -> Result<(), String>;
52//!
53//!     /// This method is ONLY available on Linux.
54//!     #[sys_function(include(linux))]
55//!     pub fn update_kernel(&self);
56//!
57//!     /// This method is available on Linux and macOS, but NOT Windows.
58//!     #[sys_function(exclude(windows))]
59//!     pub fn posix_magic(&self);
60//! }
61//!
62//! // You then implement the specific logic for each platform:
63//! impl SystemManager {
64//!     #[cfg(any(target_os = "linux", target_os = "macos", target_os = "windows"))]
65//!     fn reboot_impl(&self) -> Result<(), String> {
66//!         Ok(())
67//!     }
68//!
69//!     #[cfg(target_os = "linux")]
70//!     fn update_kernel_impl(&self) {
71//!         println!("Updating Linux kernel...");
72//!     }
73//!
74//!     #[cfg(any(target_os = "linux", target_os = "macos"))]
75//!     fn posix_magic_impl(&self) {
76//!         println!("Running POSIX specific logic");
77//!     }
78//! }
79//! ```
80//!
81//! ### 2. Using `#[sys_struct]`
82//!
83//! This creates handy type aliases for platform-specific builds and allows verifying trait implementations.
84//!
85//! ```rust
86//! # use platify::sys_struct;
87//! // 1. Generates `HandleWindows` alias on Windows.
88//! // 2. Asserts at compile time that `Handle` implements `Send` and `Sync`.
89//! #[sys_struct(traits(Send, Sync), include(windows))]
90//! pub struct Handle {
91//!     handle: u64,
92//! }
93//!
94//! // Generated code roughly looks like:
95//! //
96//! // #[cfg(target_os = "windows")]
97//! // pub type HandleWindows = Handle;
98//! //
99//! // #[cfg(target_os = "windows")]
100//! // const _: () = {
101//! //     fn _assert_traits<T: Send + Sync + ?Sized>() {}
102//! //     fn _check() { _assert_traits::<Handle>(); }
103//! // };
104//! ```
105//!
106//! ### 3. Using `#[sys_trait_function]`
107//!
108//! This allows defining methods in a trait that only exist on specific platforms.
109//!
110//! ```rust
111//! # use platify::sys_trait_function;
112//! trait DesktopEnv {
113//!     /// Only available on Linux
114//!     #[sys_trait_function(include(linux))]
115//!     fn get_wm_name(&self) -> String;
116//! }
117//! ```
118//!
119//! ### 4. Using `#[platform_mod]`
120//!
121//! This creates module aliases backed by specific files (e.g., `linux.rs`, `windows.rs`).
122//!
123//! **Note on Visibility:** The actual platform module (e.g., `mod linux;`) inherits the visibility you declare (`pub`), making it accessible to consumers.
124//!                         However, the generic alias (`mod driver;`) is generated as a **private** use-statement to be used internally.
125//!
126//! ```rust,ignore
127//! // Assumes existence of `src/linux.rs` and `src/windows.rs`
128//!
129//! #[platform_mod(include(linux, windows))]
130//! pub mod driver;
131//!
132//! // --- Internal Usage (Platform Agnostic) ---
133//! // Inside this file, we use the private alias `driver`.
134//! fn init() {
135//!     let device = driver::Device::new();
136//! }
137//! ```
138//!
139//! **External Consumer Usage:**
140//!
141//! ```rust,ignore
142//! // Users of your crate must explicitly choose the platform module.
143//! // 'driver' is not visible here.
144//! #[cfg(target_os = "linux")]
145//! use my_crate::linux::Device;
146//! ```
147
148use proc_macro::TokenStream;
149use proc_macro2::{Span as Span2, TokenStream as TokenStream2};
150use quote::{format_ident, quote, ToTokens as _};
151use std::collections::{BTreeSet, HashSet};
152use syn::parse::{Parse, ParseStream};
153use syn::spanned::Spanned as _;
154use syn::{
155    parenthesized, parse, parse_macro_input, token, Attribute, Error, FnArg, ForeignItemFn,
156    GenericParam, ItemFn, ItemMod, ItemStruct, ItemUse, Pat, PatType, ReturnType, Signature,
157    TraitItemFn, UseTree, Visibility,
158};
159
160/// Applies platform configuration to trait method definitions.
161///
162/// Use this inside a `trait` definition to limit methods to specific platforms.
163///
164/// # Options
165///
166/// - `include(...)`: Whitelist of platforms. Options: `linux`, `macos`, `windows`, `all`, `posix`.
167/// - `exclude(...)`: Blacklist of platforms. Removes them from the included set.
168#[proc_macro_attribute]
169pub fn sys_trait_function(attr: TokenStream, item: TokenStream) -> TokenStream {
170    let attr = parse_macro_input!(attr as AttrOptions);
171    let cfg_attr = attr.convert_to_cfg_attr();
172
173    let trait_fn = parse_macro_input!(item as TraitItemFn);
174
175    quote! {
176        #cfg_attr
177        #trait_fn
178    }
179    .into()
180}
181
182/// Generates a platform-dependent method implementation.
183///
184/// This attribute macro acts as a dispatcher. It applies `#[cfg(...)]` attributes based on the
185/// provided configuration and generates a default body that calls a platform-specific implementation
186/// (e.g., `fn foo()` calls `Self::foo_impl()`).
187///
188/// # Options
189///
190/// - `include(...)`: Whitelist of platforms. Options: `linux`, `macos`, `windows`, `all`, `posix`.
191/// - `exclude(...)`: Blacklist of platforms. Removes them from the included set.
192///
193/// If `include` is omitted, it defaults to `all` (minus any exclusions).
194///
195/// # Logic
196///
197/// 1. Calculates the set of allowed platforms: `(include OR all) - exclude`.
198/// 2. Applies `#[cfg(any(target_os = "..."))]` to the method.
199/// 3. Generates a default implementation: `fn foo(&self) { Self::foo_impl(self) }`.
200///
201/// # Requirements
202///
203/// The implementing type must define the corresponding `_impl` method.
204#[proc_macro_attribute]
205pub fn sys_function(attr: TokenStream, item: TokenStream) -> TokenStream {
206    let attr = parse_macro_input!(attr as AttrOptions);
207    let cfg_attr = attr.convert_to_cfg_attr();
208
209    let struct_info = match parse::<ForeignItemFn>(item.clone()) {
210        Ok(foreign_item_fn) => foreign_item_fn,
211        Err(_) => {
212            return match parse::<ItemFn>(item) {
213                Ok(item_fn) => {
214                    quote! {
215                        #cfg_attr
216                        #item_fn
217                    }
218                }
219                Err(err) => err.to_compile_error(),
220            }
221            .into()
222        }
223    };
224
225    let ForeignItemFn {
226        attrs,
227        vis,
228        sig,
229        semi_token: _,
230    } = struct_info;
231
232    let &Signature {
233        constness: _,
234        ref asyncness,
235        ref unsafety,
236        abi: _,
237        fn_token: _,
238        ref ident,
239        ref generics,
240        paren_token: _,
241        ref inputs,
242        ref variadic,
243        ref output,
244    } = &sig;
245
246    let sys_ident = format_ident!("{ident}_impl");
247    let asyncness = asyncness
248        .as_ref()
249        .map_or_else(TokenStream2::new, |_| quote!(.await));
250    let output_semicolon = if matches!(output, ReturnType::Default) {
251        quote!(;)
252    } else {
253        TokenStream2::new()
254    };
255
256    let mut param_errors = TokenStream2::new();
257    let input_names = inputs.iter().filter_map(|fn_arg| match *fn_arg {
258		FnArg::Receiver(_) => Some(quote!(self)),
259		FnArg::Typed(PatType { ref pat, .. }) => match **pat {
260			Pat::Ident(ref pat_ident) => Some(pat_ident.ident.to_token_stream()),
261            ref other => {
262				const MSG: &str = "Complex patterns in arguments are not supported by #[sys_function]: give the argument a name";
263				param_errors.extend(Error::new(other.span(), MSG).to_compile_error());
264				None
265			},
266		},
267	});
268
269    let generic_names = generics
270        .params
271        .iter()
272        .filter_map(|generic_param| match *generic_param {
273            GenericParam::Lifetime(_) => None,
274            GenericParam::Type(ref type_param) => Some(type_param.ident.to_token_stream()),
275            GenericParam::Const(ref const_param) => Some(const_param.ident.to_token_stream()),
276        })
277        .collect::<Vec<_>>();
278    let generic_names = if generic_names.is_empty() {
279        TokenStream2::new()
280    } else {
281        quote!(::<#(#generic_names),*>)
282    };
283
284    let mut body = quote! {
285        Self::#sys_ident #generic_names(#(#input_names),*)#asyncness #output_semicolon
286    };
287    if unsafety.is_some() {
288        body = quote!(unsafe { #body });
289    }
290
291    let result = quote! {
292        #cfg_attr
293        #(#attrs)*
294        #vis #sig {
295            #body
296        }
297    };
298
299    let variadic_error = variadic
300        .as_ref()
301        .map_or_else(TokenStream2::new, |variadic| {
302            Error::new(variadic.dots.span(), "Variadic arguments are not permitted")
303                .to_compile_error()
304        });
305
306    quote! {
307        #result
308        #param_errors
309        #variadic_error
310    }
311    .into()
312}
313
314/// Generates platform-specific type aliases for a struct.
315///
316/// It preserves the original struct definition and adds type aliases that are only available
317/// on specific platforms.
318///
319/// # Options
320///
321/// - `traits(...)`: Comma-separated list of traits (e.g., `Send, Sync`) to assert at compile time.
322/// - `include(...)`: Whitelist of platforms.
323/// - `exclude(...)`: Blacklist of platforms.
324///
325/// (See [`sys_function`] for more details on include/exclude logic).
326#[proc_macro_attribute]
327pub fn sys_struct(attr: TokenStream, item: TokenStream) -> TokenStream {
328    let attr = parse_macro_input!(attr as StructOptions);
329    let cfg_attr = attr.options.convert_to_cfg_attr();
330
331    let allowed_set: BTreeSet<_> = attr.options.allowed_set(|platform| match platform {
332        Platform::All | Platform::Posix => unreachable!("Should have been expanded"),
333        Platform::Linux => ("linux", "Linux"),
334        Platform::Macos => ("macos", "MacOS"),
335        Platform::Windows => ("windows", "Windows"),
336    });
337
338    let item_struct = parse_macro_input!(item as ItemStruct);
339    let &ItemStruct {
340        ref attrs,
341        ref vis,
342        struct_token: _,
343        ref ident,
344        ref generics,
345        fields: _,
346        semi_token: _,
347    } = &item_struct;
348
349    let deprecated_attr = attrs
350        .iter()
351        .find(|next_attr| next_attr.path().is_ident("deprecated"));
352
353    let generics_names = if generics.params.is_empty() {
354        TokenStream2::new()
355    } else {
356        let generics_names = generics
357            .params
358            .iter()
359            .map(|generic_param| match *generic_param {
360                GenericParam::Lifetime(ref lifetime_param) => {
361                    lifetime_param.lifetime.to_token_stream()
362                }
363                GenericParam::Type(ref type_param) => type_param.ident.to_token_stream(),
364                GenericParam::Const(ref const_param) => const_param.ident.to_token_stream(),
365            });
366        quote!(<#(#generics_names),*>)
367    };
368
369    let aliases = allowed_set.into_iter().map(|(platform, ident_postfix)| {
370        let deprecated_attr = deprecated_attr.map_or_else(
371            TokenStream2::new,
372            |deprecated_attr| quote!(#deprecated_attr),
373        );
374        let doc_msg = format!("Platform-specific alias for [{ident}].");
375        let alias_ident = format_ident!("{ident}{ident_postfix}");
376
377        #[cfg(feature = "warn-platform-struct-usage")]
378        let warning = {
379            let warn_msg = format!("[warn-platform-struct-usage] Platform struct is explicitly used: use '{ident}' instead");
380            quote!(#[deprecated(note = #warn_msg)])
381        };
382        #[cfg(not(feature = "warn-platform-struct-usage"))]
383        let warning = TokenStream2::new();
384
385        quote! {
386            #[doc = #doc_msg]
387            #deprecated_attr
388            #warning
389            #[cfg(target_os = #platform)]
390            #vis type #alias_ident #generics_names = #ident #generics_names;
391        }
392    });
393
394    let trait_asserts = if attr.traits.is_empty() {
395        TokenStream2::new()
396    } else {
397        let cfg_attr = attr.options.convert_to_cfg_attr();
398        let traits = attr.traits;
399        let generics_where_clause = generics.where_clause.as_ref();
400
401        let separator = if traits.is_empty() {
402            TokenStream2::new()
403        } else {
404            quote!(+)
405        };
406
407        let generics_usages = if generics.params.is_empty() {
408            TokenStream2::new()
409        } else {
410            let generics_usages =
411                generics
412                    .params
413                    .iter()
414                    .map(|generic_param| match *generic_param {
415                        GenericParam::Lifetime(_) => quote!('_),
416                        GenericParam::Type(ref type_param) => type_param.ident.to_token_stream(),
417                        GenericParam::Const(ref const_param) => const_param.ident.to_token_stream(),
418                    });
419            quote!(<#(#generics_usages),*>)
420        };
421
422        quote! {
423            #cfg_attr
424            const _: () = {
425                fn _assert_traits<T: #(#traits)+* #separator ?Sized>() {}
426                fn _check #generics() #generics_where_clause { _assert_traits::<#ident #generics_usages>(); }
427            };
428        }
429    };
430
431    quote! {
432        #(#aliases)*
433        #cfg_attr
434        #item_struct
435        #trait_asserts
436    }
437    .into()
438}
439
440/// Declares a platform-dependent module backed by OS-specific source files.
441///
442/// This attribute simplifies the management of platform-specific code modules. Instead of manually
443/// writing multiple `#[cfg(...)] mod ...;` blocks, you define a single logical module name.
444/// The macro expects corresponding files (e.g., `linux.rs`, `windows.rs`) to exist in the same directory.
445///
446/// # Options
447///
448/// Same as [`sys_function`]: `include(...)` and `exclude(...)` determine which platform modules are generated.
449///
450/// # Visibility Behavior
451///
452/// This macro enforces a strict separation between **internal convenience** and **external access**:
453///
454/// 1. **The Module (External):** The actual platform module (e.g., `mod linux;`) **inherits** the visibility you declared.
455///    If you write `pub mod driver;`, the generated `mod linux;` will be public.
456/// 2. **The Alias (Internal):** The logical name you specified (e.g., `driver`) is generated as a **private use-alias**.
457///
458/// **Why?** This ensures that external consumers of your crate must be explicit about the platform they are accessing
459/// (e.g., `my_crate::linux::MyStruct`), while allowing you to use the generic name (e.g., `driver::MyStruct`)
460/// conveniently within your own code.
461#[proc_macro_attribute]
462pub fn platform_mod(attr: TokenStream, item: TokenStream) -> TokenStream {
463    struct DModInfo {
464        attrs: Vec<Attribute>,
465        vis: Visibility,
466        ident: proc_macro2::Ident,
467    }
468
469    let attr = parse_macro_input!(attr as AttrOptions);
470    let allowed_set: BTreeSet<_> = attr.allowed_set(|platform| match platform {
471        Platform::All | Platform::Posix => unreachable!("Should have been expanded"),
472        Platform::Linux => "linux",
473        Platform::Macos => "macos",
474        Platform::Windows => "windows",
475    });
476
477    let mod_info = match parse::<ItemUse>(item.clone()) {
478        Ok(item_use) => {
479            let ItemUse {
480                attrs,
481                vis,
482                use_token: _,
483                leading_colon,
484                tree,
485                semi_token: _,
486            } = item_use;
487
488            if let Some(leading_colon) = leading_colon {
489                return Error::new(
490				    leading_colon.span(),
491				    "#[platform_mod] does not support absolute paths (leading `::`). Please use a local identifier"
492			    ).to_compile_error().into();
493            }
494
495            let use_ident = match tree {
496                UseTree::Name(use_name) => use_name.ident,
497                other @ (UseTree::Path(_)
498                | UseTree::Rename(_)
499                | UseTree::Glob(_)
500                | UseTree::Group(_)) => {
501                    return Error::new(
502					    other.span(),
503					    "#[platform_mod] on `use` statements only supports simple direct aliases (e.g., `use name;`)"
504				    ).to_compile_error().into();
505                }
506            };
507
508            DModInfo {
509                attrs,
510                vis,
511                ident: use_ident,
512            }
513        }
514        Err(_) => match parse::<ItemMod>(item) {
515            Ok(item_mod) => {
516                let item_mod_span = item_mod.span();
517
518                let ItemMod {
519                    attrs,
520                    vis,
521                    unsafety,
522                    mod_token: _,
523                    ident,
524                    content,
525                    semi: _,
526                } = item_mod;
527
528                if let Some(unsafety) = unsafety {
529                    return Error::new(
530                        unsafety.span(),
531                        "#[platform_mod] does not support `unsafe` modules",
532                    )
533                    .to_compile_error()
534                    .into();
535                }
536
537                if content.is_some() {
538                    return Error::new(
539					    item_mod_span,
540					    "#[platform_mod] does not support inline modules with a body `{ ... }`.\n\
541					    Please use a declaration like `mod name;` to allow swapping the file based on the platform."
542				    ).to_compile_error().into();
543                }
544
545                DModInfo { attrs, vis, ident }
546            }
547            Err(_) => {
548                return Error::new(
549				    Span2::call_site(),
550				    "#[platform_mod] expected a `mod declaration` (e.g., `mod foo;`) or a `use statement` (e.g., `use foo;`)"
551			    ).to_compile_error().into();
552            }
553        },
554    };
555
556    let DModInfo { attrs, vis, ident } = mod_info;
557
558    let mods = allowed_set.into_iter().map(|platform| {
559        let platform_ident = format_ident!("{platform}");
560
561        quote! {
562            #[cfg(target_os = #platform)]
563            #(#attrs)*
564            #vis mod #platform_ident;
565            #[cfg(target_os = #platform)]
566            #(#attrs)*
567            use #platform_ident as #ident;
568        }
569    });
570
571    quote!(#(#mods)*).into()
572}
573
574// ##################################### IMPLEMENTATION #####################################
575
576mod keywords {
577    use syn::custom_keyword;
578
579    custom_keyword!(traits);
580
581    custom_keyword!(exclude);
582    custom_keyword!(include);
583
584    custom_keyword!(all);
585    custom_keyword!(posix);
586    custom_keyword!(linux);
587    custom_keyword!(macos);
588    custom_keyword!(windows);
589}
590
591#[derive(Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd)]
592enum Platform {
593    All,
594    Posix,
595    Linux,
596    Macos,
597    Windows,
598}
599
600impl Platform {
601    #[must_use]
602    fn expand(self) -> Vec<Self> {
603        match self {
604            Self::All => vec![Self::Linux, Self::Macos, Self::Windows],
605            Self::Posix => vec![Self::Linux, Self::Macos],
606            Self::Linux | Self::Macos | Self::Windows => vec![self],
607        }
608    }
609}
610
611impl Parse for Platform {
612    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
613        let lookahead = input.lookahead1();
614        if lookahead.peek(keywords::all) {
615            input.parse::<keywords::all>()?;
616            Ok(Self::All)
617        } else if lookahead.peek(keywords::posix) {
618            input.parse::<keywords::posix>()?;
619            Ok(Self::Posix)
620        } else if lookahead.peek(keywords::linux) {
621            input.parse::<keywords::linux>()?;
622            Ok(Self::Linux)
623        } else if lookahead.peek(keywords::macos) {
624            input.parse::<keywords::macos>()?;
625            Ok(Self::Macos)
626        } else if lookahead.peek(keywords::windows) {
627            input.parse::<keywords::windows>()?;
628            Ok(Self::Windows)
629        } else {
630            Err(lookahead.error())
631        }
632    }
633}
634
635struct AttrOptions {
636    span: Span2,
637    exclude: HashSet<Platform>,
638    include: HashSet<Platform>,
639}
640
641impl AttrOptions {
642    #[must_use]
643    fn allowed_set<B: FromIterator<O>, M: Fn(Platform) -> O, O>(&self, mapping: M) -> B {
644        let all_includes = self
645            .include
646            .iter()
647            .copied()
648            .flat_map(Platform::expand)
649            .collect::<HashSet<_>>();
650        let all_excludes = self
651            .exclude
652            .iter()
653            .copied()
654            .flat_map(Platform::expand)
655            .collect::<HashSet<_>>();
656        all_includes
657            .difference(&all_excludes)
658            .map(|platform| mapping(*platform))
659            .collect()
660    }
661
662    #[must_use]
663    fn convert_to_cfg_attr(&self) -> TokenStream2 {
664        let allowed_set: BTreeSet<_> = self.allowed_set(|platform| match platform {
665            Platform::All | Platform::Posix => unreachable!("Should have been expanded"),
666            Platform::Linux => "linux",
667            Platform::Macos => "macos",
668            Platform::Windows => "windows",
669        });
670
671        let error = if allowed_set.is_empty() {
672            Error::new(
673				self.span,
674				"Configuration excludes all platforms: 'include' and 'exclude' cancel each other out",
675			)
676				.to_compile_error()
677        } else {
678            TokenStream2::new()
679        };
680
681        let mut cfg_attrs = quote!(#(target_os = #allowed_set),*);
682        if allowed_set.len() != 1 {
683            cfg_attrs = quote!(any(#cfg_attrs));
684        }
685
686        quote! {
687            #error
688            #[cfg(#cfg_attrs)]
689        }
690    }
691}
692
693impl Parse for AttrOptions {
694    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
695        parse_attributes(input, false).map(|options| {
696            let StructOptions { options, traits } = options;
697            assert_eq!(traits.len(), 0, "Implementation error");
698            options
699        })
700    }
701}
702
703struct StructOptions {
704    options: AttrOptions,
705    traits: Vec<syn::Path>,
706}
707
708impl Parse for StructOptions {
709    fn parse(input: ParseStream<'_>) -> syn::Result<Self> {
710        parse_attributes(input, true)
711    }
712}
713
714fn parse_attributes(input: ParseStream<'_>, allow_traits: bool) -> syn::Result<StructOptions> {
715    let mut result = StructOptions {
716        options: AttrOptions {
717            span: input.span(),
718            exclude: HashSet::default(),
719            include: HashSet::default(),
720        },
721        traits: Vec::default(),
722    };
723
724    while !input.is_empty() {
725        let lookahead = input.lookahead1();
726
727        if allow_traits && lookahead.peek(keywords::traits) {
728            input.parse::<keywords::traits>()?;
729
730            let content;
731            parenthesized!(content in input);
732
733            let traits = content.parse_terminated(syn::Path::parse, token::Comma)?;
734            result.traits.extend(traits);
735        } else if lookahead.peek(keywords::exclude) {
736            input.parse::<keywords::exclude>()?;
737
738            let content;
739            parenthesized!(content in input);
740
741            let platforms = content.parse_terminated(Platform::parse, token::Comma)?;
742            result.options.exclude.extend(platforms);
743        } else if lookahead.peek(keywords::include) {
744            input.parse::<keywords::include>()?;
745
746            let content;
747            parenthesized!(content in input);
748
749            let platforms = content.parse_terminated(Platform::parse, token::Comma)?;
750            result.options.include.extend(platforms);
751        } else {
752            return Err(lookahead.error());
753        }
754
755        if !input.is_empty() {
756            input.parse::<token::Comma>()?;
757        }
758    }
759
760    if result.options.include.is_empty() {
761        result.options.include.insert(Platform::All);
762    }
763
764    Ok(result)
765}