feat(openapi-gen-build): Dynamic module imports #3
No reviewers
Labels
No labels
automerge
bug
dependencies
duplicate
enhancement
help wanted
invalid
migrations
question
wontfix
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
Personal/my-rust-lib!3
Loading…
Reference in a new issue
No description provided.
Delete branch "dynamic-modules"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Review Summary
The PR replaces the naive
str::replace-basedcrate::rewriting innamespace.rswith a state-machine scanner that skips comments and string/char literals, and makeswrite_contract_modingenerate.rsauto-discover emitted modules instead of hardcodingapis/models. The README is updated accordingly.Overall the change is a clear improvement: module discovery is deterministic (sorted), and the scanner is a single O(n) pass that correctly handles line comments, nested block comments, and escaped quotes, with good unit tests. The
ConfigAPI used in the README example exists and matches.Two correctness bugs in the scanner should be fixed before merging:
'always entersScanState::Character, but a lifetime such as'a,'static, orimpl<'a>has no closing quote. The scanner then stays inCharacterstate until the next unrelated', so everycrate::path in that span is silently left unrewritten (e.g.fn f<'a>(x: &'a crate::models::Foo)keepscrate::models). This produces broken generated code at the consumer side without any error.r"...",r#"..."#,br"...", etc. are handled as ordinary strings. A raw string containing a double quote — which is legal — prematurely closes the string state and causes thecrate::text inside the literal to be rewritten, corrupting the literal.Minor robustness concern:
write_contract_modwill declare any.rsfile found (including dotfiles such as.models.rs, producing invalidpub mod .models;) and will emit a duplicatepub mod foo;if bothfoo.rsandfoo/mod.rsexist.No security issues found (module names come from directory entries and cannot contain path separators). Performance is acceptable for a build-time script.
*🤖 Review by opencode (opencode/deepseek-v4-flash-free) — verdict: changes
Code review findings from opencode.
@ -83,0 +89,4 @@};if path.is_file() {if path.extension().and_then(|ext| ext.to_str()) == Some("rs") && name != "mod.rs" {[bug] Module discovery declares every
.rsfile exceptmod.rs, without filtering dotfiles or editor artifacts. A.models.rsormodels.rs~-style stray file yields invalid declarations such aspub mod .models;, and if bothfoo.rsandfoo/mod.rsexist the generatedmod.rscontainspub mod foo;twice, which fails to compile. Consider skipping names that are not valid Rust identifiers and de-duplicating the module list.@ -31,0 +75,4 @@}let byte = bytes[index];if byte == b'"' {[bug] Raw string literals (
r"...",r#"..."#,br"...",c"...") are not distinguished from plain strings. Because a raw string may legally contain", the string state closes early at that quote and anycrate::text inside the literal is treated as code and rewritten, corrupting the literal (e.g.r#"let s = "crate::apis";"#). Generated Rust output can contain such literals (doc examples, etc.); the scanner should recognize ther/br/crprefix and the matching#-delimited terminator.@ -31,0 +79,4 @@output.push(byte);return (ScanState::String(false), index + 1);}if byte == b'\'' {[bug] A
'is always treated as the start of a char literal, but in Rust'also introduces a lifetime ('a,'static,impl<'a>,&'a ...) which has no closing quote. The scanner then remains inScanState::Characteruntil an unrelated'later in the file, so everycrate::path inside that span is silently NOT rewritten (e.g.fn f<'a>(x: &'a crate::models::Foo)keepscrate::models, breaking the consumer build). Consider detecting'followed by an identifier char (and not\) as a lifetime instead of a char literal, and add a test with a lifetime adjacent to acrate::path.@ -31,0 +161,4 @@&& (index + 5 == bytes.len() || !is_ident(bytes[index + 5]))}fn skip_path_whitespace(bytes: &[u8], mut index: usize) -> usize {[style]
skip_path_whitespaceonly skips spaces and tabs, but Rust allows newline whitespace around path separators (crate\n::foois valid). Such a path would not be rewritten. This is unlikely in generated output, but skipping all ASCII whitespace (including\r,\n) would make the scanner fully consistent.Let me verify the raw-string closing semantics against rustc and run the tests.
The bash tool can't spawn processes in this environment, so I'll verify the remaining questions statically. Let me check how the namespace is wired to the module tree.
{"summary": "# Review Summary\n\nA focused improvement to
openapi-gen-buildthat (1) makeswrite_contract_moddiscover generated modules dynamically instead of hardcodingapis/models, and (2) replaces the naive substring-basedcrate::rewriting with a small hand-rolled scanner that respects comments, string/char/raw-string literals, and lifetimes. The PR is well tested and the README updates match the implementation.\n\nCorrectness: The scanner is careful — line/block/nested comments, escaped quotes, raw strings with varying hash counts, character literals and lifetimes are all handled, and grouped imports likeuse crate::{apis, models};are rewritten correctly. I verified the namespace wiring:Config::generatecomputesns = format!(\"{}::{}\", namespace, spec.name), so rewritten paths (crate::generated::contract::apis) line up with the real module tree (generated/mod.rs→pub mod contract;→ discovered modules). Backward compatibility holds: for the defaultrustgenerator, discovery yieldsapis/models, matching the previous hardcoded output. The raw-string termination point (\"followed by ≥ N hashes) matches rustc's lexer rule, and re-scanning surplus hashes as code is byte-for-byte benign for valid input. I could not run the test suite in this environment, but the added tests cover the important edge cases.\n\nMinor observations (non-blocking):\n-character_literal_enddoes not recognize\\xNNhex-escaped char literals, so such literals are scanned as code. This cannot produce wrong output today (a single-char literal can never containcrate::), but it is an inconsistency in the lexer worth handling or at least covering with a test.\n-write_contract_modcould emit duplicatepub modlines if a generator ever emitted bothfoo.rsandfoo/mod.rs; a cheap dedup would make it robust.\n\nPerformance: Linear scan, per-byte prefix checks fail fast; no concerns. Security: None — all writes target the generator output directory. Maintainability: The scanner is well documented and the state machine is clear; the README accurately describes the new behavior.","verdict": "approve","comments": [{"file": "crates/openapi-gen-build/src/namespace.rs","line": 157,"severity": "style","message": "character_literal_endhandles\\u{...}and single-char escapes but not\\xNNhex escapes: for a literal like'\\x63', theelsebranch consumes only thex(via UTF-8 char length) and then fails to find the closing quote, returningNone. The'is then treated as code. This is currently harmless — a single-char literal can never contain the textcrate::, so no incorrect rewrite can result — but it is an inconsistency in the hand-rolled lexer. Consider handling\\xNN(and ideally\\u{...}already done) explicitly, or add a regression test asserting'\\xNN'is preserved, so future maintainers don't assume all char-literal syntax is covered."},{"file": "crates/openapi-gen-build/src/generate.rs","line": 91,"severity": "style","message": "write_contract_modpushes both afoo.rsfile and afoo/directory (withmod.rs) as candidates, then emitspub mod foo;for each. If a generator output contained both afoo.rsandfoo/mod.rs, the sortedmoduleslist would containfootwice, producing duplicate module declarations and a compile error. The previous hardcodedapis/modelsoutput could not hit this. A cheap fix is to dedup after sorting (or collect into aBTreeSet). Real openapi-generator outputs don't collide today, so this is low priority."}]}*🤖 Review by opencode (opencode/deepseek-v4-flash-free) — verdict: comment