diff --git a/Cargo.lock b/Cargo.lock index 64b75e04a7d..358a6d77559 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -181,7 +181,7 @@ checksum = "ebb4bd301db2e2ca1f5be131c24eb8ebf2d9559bc3744419e93baf8ddea7e670" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.99", ] [[package]] @@ -228,7 +228,7 @@ dependencies = [ "regex", "rustc-hash", "shlex", - "syn", + "syn 2.0.99", ] [[package]] @@ -529,7 +529,7 @@ dependencies = [ "proc-macro2", "pulldown-cmark", "quote", - "syn", + "syn 2.0.102", ] [[package]] @@ -582,7 +582,7 @@ dependencies = [ "nom", "proc-macro2", "quote", - "syn", + "syn 2.0.99", ] [[package]] @@ -872,7 +872,7 @@ checksum = "1b4464d46ce68bfc7cb76389248c7c254def7baca8bece0693b02b83842c4c88" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.99", ] [[package]] @@ -1427,7 +1427,7 @@ checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.99", ] [[package]] @@ -1650,7 +1650,7 @@ checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.99", ] [[package]] @@ -2311,7 +2311,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.99", ] [[package]] @@ -2445,7 +2445,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn", + "syn 2.0.99", ] [[package]] @@ -2926,7 +2926,7 @@ checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.99", ] [[package]] @@ -3133,7 +3133,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn", + "syn 2.0.99", ] [[package]] @@ -3175,6 +3175,16 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn" +version = "2.0.102" +source = "git+https://github.com/dtolnay/syn?rev=7680cb0c1d7cbf812118081dbcdfe5bc67083488#7680cb0c1d7cbf812118081dbcdfe5bc67083488" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "sys-locale" version = "0.3.1" @@ -3296,7 +3306,7 @@ checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.99", ] [[package]] @@ -3468,7 +3478,7 @@ checksum = "ac73887f47b9312552aa90ef477927ff014d63d1920ca8037c6c1951eab64bb1" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.99", ] [[package]] @@ -3832,7 +3842,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.99", "wasm-bindgen-shared", ] @@ -3854,7 +3864,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.99", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -4164,7 +4174,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.99", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 8af48eec47b..753b02738ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,14 +14,14 @@ members = [ repository = "https://github.com/clap-rs/clap" license = "MIT OR Apache-2.0" edition = "2021" -rust-version = "1.74" # MSRV +rust-version = "1.74" # MSRV include = [ "build.rs", "src/**/*", "Cargo.toml", "LICENSE*", "README.md", - "examples/**/*" + "examples/**/*", ] [workspace.lints.rust] @@ -32,6 +32,8 @@ unsafe_op_in_unsafe_fn = "warn" unused_lifetimes = "warn" unused_macro_rules = "warn" unused_qualifications = "warn" +# For testing with default_field_values +unexpected_cfgs = { level = "warn", check-cfg = ['cfg(nightly)'] } [workspace.lints.clippy] bool_assert_comparison = "allow" @@ -63,7 +65,7 @@ invalid_upcast_comparisons = "warn" large_digit_groups = "warn" large_stack_arrays = "warn" large_types_passed_by_value = "warn" -let_and_return = "allow" # sometimes good to name what you are returning +let_and_return = "allow" # sometimes good to name what you are returning linkedlist = "warn" lossy_float_literal = "warn" macro_use_imports = "warn" @@ -112,13 +114,7 @@ name = "clap" version = "4.5.40" description = "A simple to use, efficient, and full-featured Command Line Argument Parser" categories = ["command-line-interface"] -keywords = [ - "argument", - "cli", - "arg", - "parser", - "parse" -] +keywords = ["argument", "cli", "arg", "parser", "parse"] repository.workspace = true license.workspace = true edition.workspace = true @@ -136,30 +132,23 @@ features = ["unstable-doc"] shared-version = true tag-name = "v{{version}}" pre-release-replacements = [ - {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, - {file="CHANGELOG.md", search="\\.\\.\\.HEAD", replace="...{{tag_name}}", exactly=1}, - {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, - {file="CHANGELOG.md", search="", replace="\n## [Unreleased] - ReleaseDate\n", exactly=1}, - {file="CHANGELOG.md", search="", replace="\n[Unreleased]: https://github.com/clap-rs/clap/compare/{{tag_name}}...HEAD", exactly=1}, - {file="CITATION.cff", search="^date-released: ....-..-..", replace="date-released: {{date}}"}, - {file="CITATION.cff", search="^version: .+\\..+\\..+", replace="version: {{version}}"}, - {file="src/lib.rs", search="blob/v.+\\..+\\..+/CHANGELOG.md", replace="blob/v{{version}}/CHANGELOG.md", exactly=1}, + { file = "CHANGELOG.md", search = "Unreleased", replace = "{{version}}", min = 1 }, + { file = "CHANGELOG.md", search = "\\.\\.\\.HEAD", replace = "...{{tag_name}}", exactly = 1 }, + { file = "CHANGELOG.md", search = "ReleaseDate", replace = "{{date}}", min = 1 }, + { file = "CHANGELOG.md", search = "", replace = "\n## [Unreleased] - ReleaseDate\n", exactly = 1 }, + { file = "CHANGELOG.md", search = "", replace = "\n[Unreleased]: https://github.com/clap-rs/clap/compare/{{tag_name}}...HEAD", exactly = 1 }, + { file = "CITATION.cff", search = "^date-released: ....-..-..", replace = "date-released: {{date}}" }, + { file = "CITATION.cff", search = "^version: .+\\..+\\..+", replace = "version: {{version}}" }, + { file = "src/lib.rs", search = "blob/v.+\\..+\\..+/CHANGELOG.md", replace = "blob/v{{version}}/CHANGELOG.md", exactly = 1 }, ] [features] -default = [ - "std", - "color", - "help", - "usage", - "error-context", - "suggestions", -] -debug = ["clap_builder/debug", "clap_derive?/debug"] # Enables debug messages -unstable-doc = ["clap_builder/unstable-doc", "derive"] # for docs.rs +default = ["std", "color", "help", "usage", "error-context", "suggestions"] +debug = ["clap_builder/debug", "clap_derive?/debug"] # Enables debug messages +unstable-doc = ["clap_builder/unstable-doc", "derive"] # for docs.rs # Used in default -std = ["clap_builder/std"] # support for no_std in a backwards-compatible way +std = ["clap_builder/std"] # support for no_std in a backwards-compatible way color = ["clap_builder/color"] help = ["clap_builder/help"] usage = ["clap_builder/usage"] @@ -167,18 +156,29 @@ error-context = ["clap_builder/error-context"] suggestions = ["clap_builder/suggestions"] # Optional -deprecated = ["clap_builder/deprecated", "clap_derive?/deprecated"] # Guided experience to prepare for next breaking release (at different stages of development, this may become default) +deprecated = [ + "clap_builder/deprecated", + "clap_derive?/deprecated", +] # Guided experience to prepare for next breaking release (at different stages of development, this may become default) derive = ["dep:clap_derive"] -cargo = ["clap_builder/cargo"] # Disable if you're not using Cargo, enables Cargo-env-var-dependent macros +cargo = [ + "clap_builder/cargo", +] # Disable if you're not using Cargo, enables Cargo-env-var-dependent macros wrap_help = ["clap_builder/wrap_help"] env = ["clap_builder/env"] # Use environment variables during arg parsing -unicode = ["clap_builder/unicode"] # Support for unicode characters in arguments and help messages -string = ["clap_builder/string"] # Allow runtime generated strings +unicode = [ + "clap_builder/unicode", +] # Support for unicode characters in arguments and help messages +string = ["clap_builder/string"] # Allow runtime generated strings # In-work features -unstable-v5 = ["clap_builder/unstable-v5", "clap_derive?/unstable-v5", "deprecated"] +unstable-v5 = [ + "clap_builder/unstable-v5", + "clap_derive?/unstable-v5", + "deprecated", +] unstable-ext = ["clap_builder/unstable-ext"] -unstable-styles = ["clap_builder/unstable-styles"] # deprecated +unstable-styles = ["clap_builder/unstable-styles"] # deprecated unstable-derive-ui-tests = [] unstable-markdown = ["clap_derive/unstable-markdown"] @@ -193,7 +193,11 @@ clap_derive = { path = "./clap_derive", version = "=4.5.40", optional = true } trybuild = "1.0.91" rustversion = "1.0.15" # Cutting out `filesystem` feature -trycmd = { version = "0.15.3", default-features = false, features = ["color-auto", "diff", "examples"] } +trycmd = { version = "0.15.3", default-features = false, features = [ + "color-auto", + "diff", + "examples", +] } jiff = "0.2.3" snapbox = { version = "0.6.16", features = ["term-svg"] } shlex = "1.3.0" diff --git a/clap_derive/Cargo.toml b/clap_derive/Cargo.toml index 6db87ef5f99..e3e85e375f0 100644 --- a/clap_derive/Cargo.toml +++ b/clap_derive/Cargo.toml @@ -2,14 +2,11 @@ name = "clap_derive" version = "4.5.40" description = "Parse command line argument by defining a struct, derive crate." -categories = ["command-line-interface", "development-tools::procedural-macro-helpers"] -keywords = [ - "clap", - "cli", - "parse", - "derive", - "proc_macro" +categories = [ + "command-line-interface", + "development-tools::procedural-macro-helpers", ] +keywords = ["clap", "cli", "parse", "derive", "proc_macro"] repository.workspace = true license.workspace = true edition.workspace = true @@ -30,12 +27,15 @@ proc-macro = true bench = false [dependencies] -syn = { version = "2.0.8", features = ["full"] } +# https://github.com/dtolnay/syn/pull/1870 +syn = { version = "2.0.8", features = [ + "full", +], git = "https://github.com/dtolnay/syn", rev = "7680cb0c1d7cbf812118081dbcdfe5bc67083488" } quote = "1.0.9" proc-macro2 = "1.0.69" heck = "0.5.0" -pulldown-cmark = { version = "0.13.0", default-features = false, optional = true} -anstyle = {version ="1.0.10", optional = true} +pulldown-cmark = { version = "0.13.0", default-features = false, optional = true } +anstyle = { version = "1.0.10", optional = true } [features] default = [] diff --git a/clap_derive/src/item.rs b/clap_derive/src/item.rs index ab32918e5f4..1f761df9009 100644 --- a/clap_derive/src/item.rs +++ b/clap_derive/src/item.rs @@ -21,7 +21,9 @@ use syn::DeriveInput; use syn::{self, ext::IdentExt, spanned::Spanned, Attribute, Field, Ident, LitStr, Type, Variant}; use crate::attr::{AttrKind, AttrValue, ClapAttr, MagicAttrName}; -use crate::utils::{extract_doc_comment, format_doc_comment, inner_type, is_simple_ty, Sp, Ty}; +use crate::utils::{ + extract_doc_comment, format_doc_comment, inner_type, is_simple_ty, DefaultField, Sp, Ty, +}; /// Default casing style for generated arguments. pub(crate) const DEFAULT_CASING: CasingStyle = CasingStyle::Kebab; @@ -34,7 +36,7 @@ pub(crate) struct Item { name: Name, casing: Sp, env_casing: Sp, - ty: Option, + pub(crate) ty: Option, doc_comment: Vec, methods: Vec, deprecations: Vec, @@ -204,16 +206,35 @@ impl Item { let name = field.ident.clone().unwrap(); let ident = field.ident.clone().unwrap(); let span = field.span(); - let ty = Ty::from_syn_ty(&field.ty); + let default_field = DefaultField::from_field_type(field.ty.clone()); + let ty = Ty::from_syn_ty(&default_field.ty); let kind = Sp::new(Kind::Arg(ty), span); + let default_value_method = if let Some((_, expr)) = default_field.default { + let ty = &default_field.ty; + let val = quote_spanned!(expr.span()=> { + static DEFAULT_VALUE: ::std::sync::OnceLock = ::std::sync::OnceLock::new(); + let s = DEFAULT_VALUE.get_or_init(|| { + let val: #ty = #expr; + ::std::string::ToString::to_string(&val) + }); + let s: &'static str = &*s; + s + }); + let raw_ident = Ident::new("default_value", expr.span()); + let method = Method::new(raw_ident, val); + Some(method) + } else { + None + }; let mut res = Self::new( Name::Derived(name), ident, - Some(field.ty.clone()), + Some(default_field.ty), struct_casing, env_casing, kind, ); + res.methods.extend(default_value_method); let parsed_attrs = ClapAttr::parse_all(&field.attrs)?; res.infer_kind(&parsed_attrs)?; res.push_attrs(&parsed_attrs)?; @@ -1215,7 +1236,7 @@ impl Kind { } } -#[derive(Clone)] +#[derive(Clone, Debug)] pub(crate) struct Method { name: Ident, args: TokenStream, diff --git a/clap_derive/src/utils/default_field_value.rs b/clap_derive/src/utils/default_field_value.rs new file mode 100644 index 00000000000..44b5fade824 --- /dev/null +++ b/clap_derive/src/utils/default_field_value.rs @@ -0,0 +1,49 @@ +//! Support for named fields with default field values + +use syn::{ + parse::{Parse, ParseStream}, + Expr, Token, Type, +}; + +pub(crate) struct DefaultField { + pub(crate) ty: Type, + /// Default value: `field_name: i32 = 1` + /// + /// `#![feature(default_field_values)]` + pub(crate) default: Option<(Token![=], Expr)>, +} + +impl Parse for DefaultField { + fn parse(input: ParseStream<'_>) -> syn::Result { + let field = input.parse()?; + let default = if input.peek(Token![=]) { + let eq_token = input.parse()?; + Some((eq_token, input.parse()?)) + } else { + None + }; + + Ok(Self { ty: field, default }) + } +} + +impl DefaultField { + pub(crate) fn from_field_type(ty: Type) -> Self { + match ty { + Type::Verbatim(stream) => { + if let Ok(parsed) = syn::parse2(stream.clone()) { + parsed + } else { + Self { + ty: Type::Verbatim(stream), + default: None, + } + } + } + other => Self { + ty: other, + default: None, + }, + } + } +} diff --git a/clap_derive/src/utils/mod.rs b/clap_derive/src/utils/mod.rs index c7e4b8e5358..8e0c98ae830 100644 --- a/clap_derive/src/utils/mod.rs +++ b/clap_derive/src/utils/mod.rs @@ -1,9 +1,11 @@ pub(crate) mod error; +mod default_field_value; mod doc_comments; mod spanned; mod ty; +pub(crate) use default_field_value::DefaultField; pub(crate) use doc_comments::extract_doc_comment; pub(crate) use doc_comments::format_doc_comment; diff --git a/clap_derive/src/utils/ty.rs b/clap_derive/src/utils/ty.rs index c38344c8dda..8a17882db40 100644 --- a/clap_derive/src/utils/ty.rs +++ b/clap_derive/src/utils/ty.rs @@ -1,5 +1,7 @@ //! Special types handling +use crate::utils::DefaultField; + use super::spanned::Sp; use syn::{ @@ -22,6 +24,10 @@ pub(crate) enum Ty { impl Ty { pub(crate) fn from_syn_ty(ty: &Type) -> Sp { use self::Ty::{Option, OptionOption, OptionVec, OptionVecVec, Other, Unit, Vec, VecVec}; + // TODO: Avoid clone here. + // Hack: If the type has a default field value, just ignore it. + let ty = DefaultField::from_field_type(ty.clone()).ty; + let ty = &ty; let t = |kind| Sp::new(kind, ty.span()); if is_unit_ty(ty) { @@ -57,6 +63,11 @@ impl Ty { pub(crate) fn inner_type(field_ty: &Type) -> &Type { let ty = Ty::from_syn_ty(field_ty); + // TODO: Avoid clone here. + // Hack: If the type has a default field value, just ignore it. + let field_ty = DefaultField::from_field_type(field_ty.clone()).ty; + // Obviously, this is really awful, and will be changed before merging. + let field_ty = Box::leak(Box::new(field_ty)); match *ty { Ty::Vec | Ty::Option => sub_type(field_ty).unwrap_or(field_ty), Ty::OptionOption | Ty::OptionVec | Ty::VecVec => { diff --git a/tests/default_field_values/main.rs b/tests/default_field_values/main.rs new file mode 100644 index 00000000000..ab28e948830 --- /dev/null +++ b/tests/default_field_values/main.rs @@ -0,0 +1,11 @@ +//! To run this test, use: +//! ``` +//! RUSTFLAGS="--cfg=nightly" cargo +nightly test --test default_field_values --features derive --features help --features usage +//! ``` +#![cfg(nightly)] +#![feature(default_field_values)] +#![cfg(feature = "derive")] +#![cfg(feature = "help")] +#![cfg(feature = "usage")] + +mod tests; diff --git a/tests/default_field_values/tests.rs b/tests/default_field_values/tests.rs new file mode 100644 index 00000000000..96631450cd6 --- /dev/null +++ b/tests/default_field_values/tests.rs @@ -0,0 +1,32 @@ +//! A module for the actual test. +//! +//! This is necessary because rustc will choke if we try and inline this module, because the (stable) parser knows +//! that default field values are unstable, and complains even for `cfg()`ed out occurrences. + +use clap::{CommandFactory, Parser}; + +// Copy of the same from tests/derive/util.rs +pub(crate) fn get_long_help() -> String { + let output = ::command() + .render_long_help() + .to_string(); + + eprintln!("\n%%% LONG_HELP %%%:=====\n{output}\n=====\n"); + eprintln!("\n%%% LONG_HELP (DEBUG) %%%:=====\n{output:?}\n=====\n"); + + output +} + +#[test] +fn default_field_value() { + #[derive(Parser, Debug, PartialEq)] + struct Opt { + arg: i32 = 3, + } + + assert_eq!(Opt { arg: 3 }, Opt::try_parse_from(["test"]).unwrap()); + assert_eq!(Opt { arg: 1 }, Opt::try_parse_from(["test", "1"]).unwrap()); + + let help = get_long_help::(); + assert!(help.contains("[default: 3]")); +}