Expand description
§Hacking on derive-deftly (HACKING.md)
Rust procedural macros are a somewhat awkward environment, and, especially, testing them can be complex.
- Required reading
- User-facing documentation
- Generated and auto-updated files in the git tree
tests/pub-export/bizarre-facade/*etc., updated bymaint/update-bizarreCargo.lock, updated bynailing-cargo update.Cargo.lock.minimal, updated byupdate-minimal-versions.- Tables of contents in various
*.md, updated bymaint/update-tocs. - Cross-references in
reference.md, updated bymaint/update-reference-xrefs
- Testing - see
tests/tests.rs - Reporting errors during template parsing and expansion
- Adding an expansion keyword
- clippy
- Updating the pinned Nightly Rust (used in tests and CI)
- Compatibility testing (and semver updates)
§Required reading
derive-deftly uses types and traits from syn and quote,
extensively.
It will be very helpful to run one of
maint/build-docs-local --dev
# or
cargo doc --document-private-items --workspaceto get a local rendering including for the internal APIs.
That will also get a rendering of this file with working links,
as target/doc/derive_deftly_macros/_doc_hacking/index.html.
NOTES.md has some ideas for the future,
which we may or may not implement.
(Comments welcome!)
§User-facing documentation
Our user-facing documentation is divided between our
rustdoc documentation and our mdbook source.
The user guide (currently only an introduction)
lives in book/src/*.
See the mdbook
documentation for more implementation.
To build all the user-facing documentation,
run maint/build-docs-local from the top-level directory,
and look in the directory it tells you.
§Generated and auto-updated files in the git tree
The git tree contains some files which are actually
maintained by scripts in maint/.
§tests/pub-export/bizarre-facade/* etc., updated by maint/update-bizarre
“Bizarre” version of derive-deftly,
used for cross-crate compatibility testing.
CI will check that these outputs are
up to date with
the normal top-level Cargo.tomls, and pub-b.rs,
from which they are generated.
§Cargo.lock, updated by nailing-cargo update.
Example lockfile.
Used in the CI tests, which (in most tests) pin all of our dependencies.
If you’re not a user of
nailing-cargo
you can update this
simply by copying a Cargo.lock made with cargo update.
§Cargo.lock.minimal, updated by update-minimal-versions.
Minimal versions of our dependencies, used for CI testing of our MSRV, etc.
update-minimal-versions runs cargo +nightly update ...,
so you have to have a Rust Nightly installed.
§Tables of contents in various *.md, updated by maint/update-tocs.
These are inserted at the <!--##toc##--> marker.
Checked by CI, but it’s only a warning if it’s not up to date.
§Cross-references in reference.md, updated by maint/update-reference-xrefs
There are x:... and c:... <div>s,
surrounding each heading describing expansions and conditions.
And indexes, at the bottom of the file.
Again, checked by CI, but it’s only a warning if it’s not up to date.
§Testing - see tests/tests.rs
derive-deftly has comprehensive tests.
But, they are complicated (mostly because testing proc macros is complicated).
You have to use a particular version of Nightly Rust.
See tests/tests.rs
for information on how to run and update the tests.
§Reporting errors during template parsing and expansion
Generally, we use only syn::Error as the error type.
Use the MakeError convenience trait’s
.error() method
to construct errors.
Often, it is a good idea to generate an error
pointing at the relevant parts of both the driver and the template;
MakeError’s implementation on
[ErrorLoc] is good for this.
§Adding an expansion keyword
You need to decide if it should be useable in ${paste }.
Generally, identifiers (or identifier-like things)
strings,
and types should,
and other things shouldn’t.
For now let’s assume it shouldn’t be useable in ${paste }.
And, you need to decide if it should be useable
as a boolean expression, in ${if } etc.
Again, for now, let’s assume not.
Add the keyword to pub enum SubstDetails
in syntax.rs.
If the keyword isn’t a Rust keyword, use its name precisely,
in lowercase.
The enum variannt should contain:
- Any arguments allowed and supplied, in their parsed form
- Markers
O::NotInPasteandO::NotInBool, as applicable.
Add the keyword to the parser in
impl ... Parse for Subst.
Use the provided keyword! macro.
For the markers, use not_in_paste? and not_in_bool?.
The compiler will now insist you add arms to various matches. Most will be obvious.
The meat of the expansion - what your new keyword means -
is in SubstDetails::expand, in expand.rs.
For an expansion which isn’t permitted in ${paste ..},
call
out.append_tokens_with()
or
out.append_tokens().
You’ll also want to add documentation to doc/reference.md,
arrangements for debug printing in macros/dbg_allkw.rs,
test cases in tests/expand/ and maybe tests/ui/,
and possibly discussion in book/src/.
§Accessing the driver
Information about the driver (and the current variant and field)
is available via framework::Context.
(Use the methods on Context, such as
field(),
to get access to the per-field and per-variant details,
rather than using Context.variant
and open-coding the error handling for None.)
§Expansion keywords with content or arguments
Parse the content from input, in the keyword! invocation.
See tmeta et al for an example.
Usually it is best to make a Rust type to represent the content or arguments, if there isn’t a suitable one already.
To parse a boolean expression, use Subst<BooleanContext>.
(Probably, in a Box, like in when).
Normally it is best to put the O::Not... markers
directly in the SubstDetails enum variant;
that makes it easier to extract them for use in the match arms.
It is fine to have thsee markers in an argument type as well.
For a sophisticated example of this,
see SubstMeta,
which allows ... as ..., except in boolean context.
For named arguments, use syntax::ParseUsingSubkeywords.
§Adding a keyword that can appear in ${paste } and/or ${CASE }
Removing O::NotInPaste marker from a SubstDetails variant
will allow the template to contain that keyword
within ${paste} and ${CASE}.
You won’t be able to call out.append_tokens any more.
Instead, you must use one of the more specific
framework::ExpansionOutput methods,
such as append_identfrag or append_idpath.
§Adding a boolean keyword
This is fairly straightforward.
Use is_enum (say) as an example.
§clippy
We do run clippy, but we turn off all style and complexity lints.
In CI, we test with a pinned version of clippy, currently 1.93.0, because clippy often introduces new lints, and we want to handle that breakage in a controlled fashion.
If your MR branch fails the clippy job, you can repro locally with:
rustup toolchain add 1.93
rustup component add clippy
cargo +1.93 clippy --locked --workspace --all-features§Updating the pinned clippy (housekeeping task)
- Update the version in
.gitlab-ci.yml, and above. - Also update the same Rust version in the
cargo-doc-alljob, and inmaint/update-dependencies. - Run the new clippy and fix or allow lints as appropriate.
- Make an MR of all of this. Commit structure is not critical for clippy lints, since we run clippy on the MR tip only.
(Typically the pinned non-nightly used for clippy and cargo doc will be older than the pinned nightly used for most tests.)
§clippy #[allow]s - strategy and policy
We put #![allow(clippy::style, clippy::complexity)]
in every top-level Rust file.
In tests, we have
#![allow(clippy::style, clippy::complexity, clippy::perf)].
(Some files which are sufficiently simple to not trigger any lints, are lacking these annotations. We’ll add them as needed.)
Feel free to add an #[allow] if it seems like
clippy wants you to make the code worse.
We often prefer code which isn’t “minimal”,
if it seems clearer, or more consistent with other nearby code,
or if it might make future edits easier.
For a clippy false positive, link to the upstream bug report,
eg
#[allow(clippy::non_minimal_cfg)] // rust-clippy/issues/13007
§Updating the pinned Nightly Rust (used in tests and CI)
The docker image and the nightly version number +nightly-YYYY-MM-DD
must be kept in sync. cargo expand will probably need updating too.
§Choose which Nightly Rust version to update to
Use this to select a corresponding Nightly Rust and container image:
https://www.chiark.greenend.org.uk/~ian/docker-tags-history/
To parse the json, You can use a rune like this:
curl https://www.chiark.greenend.org.uk/~ian/docker-tags-history/rustlang,rust/tags.2025-02-10T13:08+00:00.gz | zcat | jq -r '.results[] | select(.name | match("^nightly-bookworm$")) | .images[] | select(.architecture | match("^amd64"))'
Pick a date just before upstream Rust branched for a release, since that way
we’ll maybe have a less-buggy nightly.
Note the date YYYY-MM-DD (from the tags. part of the URL) and
use the jq rune to get the sha256 image digest.
(The Docker official way seems to be to visit
https://hub.docker.com/r/rustlang/rust/tags
and look for the nightly-bookworm tag, or whatever.
However, as far as I can tell historical information is not available,
even though the images are retained!)
§Update the nightly version number
Install the chosen nightly: rustup toolchain add nightly-YYYY-MM-DD
Then run:
TRYBUILD=overwrite MACROTEST=overwrite STDERRTEST=overwrite \
cargo +nightly-YYYY-MM-DD test --workspace --all-featuresInspect the output carefully before committing.
Use git-grep on the old nightly date string and fix all the instances.
§Update the cargo-expand version
Quite likely, you’ll need to update cargo-expand too, since it may not build with the new nightly.
Find the most recent version on crates.io,
and cargo install --locked --version 1.0.NN cargo-expand
Run the overwriting test rune, above.
Inspect the output carefully before committing.
Edit .gitlab-ci.yml with search-and-replace to fix all the occurrences.
§Prepare and merge the changes
-
Make an MR of any preparatory fixes you found you needed to make. Such changes ought to work with all compiler versions, and should be made into an MR of their own.
-
When that has merged, make an MR containing one commit:
- gitlab image update
cargo expandupdate- consequential changes to expected test outputs
Cargo.lockupdate (minimal-versions ought not to change here)
This has to be its own MR because it changes, along the way, things that the
every-committest assumes don’t change. Splitting it into multiple MRs arranges to refresh those assumptions. -
Finally, make an MR for any changes you wish to make to this doc.
§Possible version skew
The procedure given above involves picking a date, getting the
corresponding container image, and then inferring the right nightly
date to use in rustup. Normally this works.
If CI fails on the updates MR, with discrepancies in test output,
look at the version number printed by rustc at the top of the CI log.
Then add one day and use that as your rustp version, and try again.
Hopefully it will work.
§Compatibility testing (and semver updates)
New template features can be added, and that’s just a semver addition, as usual; if a breaking change to a template feature is needed, that is just a semver breaking change.
More intrusive or technical changes can cause semver breaks
in crates that export templates.
See the public API docs for define_derive_deftly
and template_export_semver_check.
Such breaking changes should be avoided if at all possible.
There are complex arrangements for testing that
compatibility isn’t broken accidentally,
mostly in compat/. (See tests/compat/README.md.)
If it is necessary to make such a totally breaking change,
consult the git history and see how it was done last time.
(git annotate on the implementation of template_export_semver_check
or git log -G on a relevant version number
may be helpful to find the relevant MR and its commits).