Harden workflow codegen module-name derivation#186
Open
sohumt123 wants to merge 1 commit into
Open
Conversation
The build script derives a Rust module name for each spec by snake-casing
its file name, but two edge cases produce broken generated code:
- Collisions: the derived name comes from the basename only and there is no
uniqueness check, so two specs that differ solely in separators or case
(e.g. `collide-test.yaml` and `collide_test.yaml`) both normalize to
`collide_test`. One workflow's generated file is silently overwritten and
the module list emits a duplicate `pub mod collide_test;`, which fails to
compile with `error[E0428]`. Because WalkDir recurses `../specs` but only
the basename is used, this can happen across different tool subdirectories.
- Interior extensions: stripping the extension with
`String::replace(".yaml", "")` removes every occurrence, and the snake-case
pass leaves stray dots in place, so `my.yaml.config.yaml` becomes the
invalid module `my.config` and breaks the build.
Extract the derivation into a `module_name` helper that strips only the
trailing extension, snake-cases the stem, replaces any remaining
non-identifier character with `_`, and prefixes `_` when the result would be
empty or start with a digit, so the output is always a valid identifier. The
build script now tracks the module name -> spec path mapping and fails fast
with an error naming both colliding specs instead of silently dropping a
workflow or emitting uncompilable code.
The helper lives in `src/module_name.rs`, pulled into the build script via
`#[path]` and into the library under `cfg(test)` so the new unit tests run
under `cargo test` (build scripts have no test harness). Verified against all
332 existing specs: the generated module names are unchanged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Discord username (optional)
(happy to share if useful for the Contributor role)
Description of changes
This is a build-tooling fix rather than a new/updated workflow. While rebuilding the crate I found two ways that innocently named spec files can silently drop a workflow or brick
cargo buildfor everyone, so this PR hardens the module-name derivation inworkflows/build.rs.Problem
build.rsturns each spec's file name into a Rust module name and emitspub mod <name>;. Two edge cases produce broken generated code:Module-name collisions (silent overwrite + build break). The name is derived from the basename via
to_case(Case::Snake)with no uniqueness check. Two specs that differ only in separators/case — e.g.collide-test.yamlandcollide_test.yaml— both normalize tocollide_test. The generated.rsfile for the first one is silently overwritten (one workflow is lost), and the module list emits a duplicatepub mod collide_test;:WalkDirrecurses all of../specsbut only the basename is used, so this can even collide across different tool subdirectories.Interior extension / stray dot. The extension is stripped with
String::replace(".yaml", ""), which removes every occurrence, and the snake-case pass passes.through unchanged. Somy.yaml.config.yamlbecomes the invalid modulemy.config:Fix
Factor the derivation into
module_name(&str) -> String(newworkflows/src/module_name.rs). It strips only the trailing.yaml/.yml, snake-cases the stem, replaces any remaining non-[A-Za-z0-9_]char with_, and prefixes_if the result is empty or starts with a digit, so the output is always a valid identifier.build.rsnow keeps aHashMap<module name, spec path>and, on collision, fails fast with a clear message naming both specs — which also prevents the silent overwrite:The helper is shared into
build.rsvia#[path]and compiled into the library under#[cfg(test)], so its unit tests actually run undercargo test(a build script has no test harness of its own). While touching that expression I also replaced the pre-existing.expect("OsStr should convert to str")with properanyhowcontext.Zero behavior change for the existing corpus: verified the new algorithm against all 332 current spec basenames — the generated module names are byte-for-byte identical, so downstream output is untouched.
Testing
New unit tests in
module_name.rs(the crate previously had no Rust tests) pin the exact mappings, including the regressions above, plus a property check that every output is a valid identifier:Integration check with local-only fixtures (not committed):
After removing the fixtures, a full rebuild of the pristine 332-spec corpus passes and
ls generated_workflows | sortdiffs empty against a pre-change snapshot (zero module-name drift).cargo fmt --check,cargo clippy -- -A warnings, andcargo clippy --tests -- -A warningsall pass.