Fix Forwarding of Doc-Comments on Return-Parameters and Variant-Fields - #818
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Variant conversion can erase an existing field comment when no matching @param exists.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Fixes forwarding of operation return and variant-field documentation to code generators.
Changes:
- Maps
@returnstags to return fields. - Maps enumerator
@paramtags to variant fields.
File summaries
| File | Description |
|---|---|
slicec/src/slice_file_converter.rs |
Adds return and variant-field comment conversion. |
Review details
- Files reviewed: 1/1 changed files
- Comments generated: 1
- Review effort level: Balanced
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
pepone
left a comment
There was a problem hiding this comment.
LGTM.
Suggestion (non-blocking)
- A regression test here would be the first for
slice_file_converter.rs, but needs no new infrastructure: a#[cfg(test)] modin the binary crate that compiles a snippet withcompile_from_strings, converts it withSliceFile::from, and asserts onField.entity_info.comment. The module below fails 3/4 onmainand passes on this branch.
slicec/src/converter_doc_comment_tests.rs (wire in with #[cfg(test)] mod converter_doc_comment_tests; in main.rs)
//! Tests that `@param` / `@returns` doc-comment tags are forwarded onto the mapped `Field`s that code-generator
//! plugins receive (operation parameters, return members, and variant fields).
//
// To wire in: add `#[cfg(test)] mod converter_doc_comment_tests;` next to `mod slice_file_converter;` in main.rs.
use crate::definition_types::*;
use slicec::compile_from_strings;
/// Compiles `slice` (asserting no errors) and returns the converted contents of the file.
fn convert(slice: &str) -> Vec<Symbol> {
let state = compile_from_strings(&[slice], None);
assert!(!state.diagnostics.has_errors(), "{:?}", state.diagnostics);
SliceFile::from(&state.files[0]).contents
}
/// Flattens a mapped doc-comment's overview into a single string (links rendered as `{@link id}`).
fn overview_of(field: &Field) -> Option<String> {
field.entity_info.comment.as_ref().map(|comment| {
comment.overview.iter().map(|component| match component {
MessageComponent::Text(text) => text.clone(),
MessageComponent::Link(link) => format!("{{@link {link}}}"),
}).collect()
})
}
fn operations(symbols: &[Symbol]) -> &[Operation] {
let Some(Symbol::Interface(interface)) = symbols.iter().find(|s| matches!(s, Symbol::Interface(_))) else {
panic!("no interface in {symbols:?}");
};
&interface.operations
}
fn variants(symbols: &[Symbol]) -> &[Variant] {
let Some(Symbol::VariantEnum(variant_enum)) = symbols.iter().find(|s| matches!(s, Symbol::VariantEnum(_))) else {
panic!("no variant enum in {symbols:?}");
};
&variant_enum.variants
}
#[test]
fn param_tags_are_forwarded_to_parameters() {
let symbols = convert("
module tests
interface I {
/// @param p: the param
op(p: string, undocumented: int32)
}
");
let op = &operations(&symbols)[0];
assert_eq!(overview_of(&op.parameters[0]), Some("the param\n".to_owned()));
assert_eq!(overview_of(&op.parameters[1]), None);
}
#[test]
fn unnamed_returns_tag_is_forwarded_to_single_return_type() {
let symbols = convert("
module tests
interface I {
/// @returns: the value
plain() -> int32
/// @returns: the stream
streamed() -> stream int32
/// @returns: the tagged value
tagged() -> tag(1) int32?
}
");
for op in operations(&symbols) {
assert_eq!(op.return_type.len(), 1, "{}", op.entity_info.identifier);
assert!(overview_of(&op.return_type[0]).is_some(), "{}", op.entity_info.identifier);
}
}
#[test]
fn named_returns_tags_are_forwarded_to_return_tuple_members() {
let symbols = convert("
module tests
interface I {
/// @returns b: second
/// @returns a: first
op() -> (a: int32, b: string, c: bool)
}
");
let op = &operations(&symbols)[0];
assert_eq!(overview_of(&op.return_type[0]), Some("first\n".to_owned()));
assert_eq!(overview_of(&op.return_type[1]), Some("second\n".to_owned()));
assert_eq!(overview_of(&op.return_type[2]), None);
}
#[test]
fn param_tags_are_forwarded_to_variant_fields() {
let symbols = convert("
module tests
enum E {
/// Variant overview.
/// @param y: the y field
A(x: int32, y: string)
B(z: int32)
}
");
let variants = variants(&symbols);
assert_eq!(overview_of(&variants[0].fields[0]), None);
assert_eq!(overview_of(&variants[0].fields[1]), Some("the y field\n".to_owned()));
assert_eq!(overview_of(&variants[1].fields[0]), None);
}Author's call
parameter.parent().return_members().len()collects aVecjust to count it, once per return member;parameter.parent().return_type.len()is the same check without the allocation, and matches howconvert_operationalready readsreturn_typedirectly.- The
is_returnbool is fine as-is.
Follow-ups, not for this PR: an unnamed @returns: on a tuple-returning operation passes validation without a lint and, with no single member to attach it to, never reaches a generator either (#819). Same shape for an enumerator @param whose name matches no field, or that sits on a basic enum (#820).
bernardnormier
left a comment
There was a problem hiding this comment.
LGTM, with a few small items not already raised by pepone or Copilot.
Nit
slice_file_converter.rs:274: "you would use" → "you use".
Reuse (author's call)
- The grammar
Message→ mapped overview conversion is now written three times in this file (get_doc_comment_for_parameterat ~L298, theFrom<&GrammarDocComment>impl at ~L392, andconvert_variant_fieldat ~L569), and the@paramfind-and-map chain twice (~L291-300 and ~L566-570). Animpl From<&GrammarMessage> for DocCommentnext to the existingFrom<&GrammarDocComment>impl, plus a smallfind_param_message(params, identifier)helper, would collapse all of them, and would letconvert_variant_fieldset the comment when constructing the field instead of building it viaconvert_struct_fieldand patching it afterwards.
Follow-up, not for this PR
- No validator rejects duplicate
@returns(or@param) tags for the same member, and both lookups usefind, so only the first message is forwarded and the rest are silently dropped. Pre-existing for@param, newly applicable to@returnsnow that they are consumed.
I did add a test originally, but it ended up not being very small or very unit-testy, so I removed it.
My Claude also flagged this minor inefficiency, but I think what the PR does is correct.
The first point about unnnamed The second point is definitely a bug. Will fix separately. |
|
I updated the wording to "you use" I introduced a helper function for the conversion of And, for your duplication findings, this is a known issue, that I'd like to fix for 0.4.1; see: #625 |
bernardnormier
left a comment
There was a problem hiding this comment.
Thanks for the fixes. One more thing before approving: several of the comments in this PR narrate the code right below them, which is exactly what we want to avoid.
Please remove
- L566
// Construct the field as normal by re-using the logic of 'convert_struct_field'.— the next line is the call toconvert_struct_field. - L300
// If this isn't a return parameter, we search the operation's doc-comment for a matching '@param' tag.— this is theelsebranch, spelled out. - L444
// Iterate through the provided file's contents, and convert each of its top-level definitions.— pre-existing, but since you touched it: it describes aforloop overcontents.
Please trim to the "why"
- L289-290: the two sentences mirror the two arms of the
match. The only non-obvious fact is the language rule: a single return type is documented by an unnamed@returns, tuple members by name. One sentence stating that rule is enough. - L569-570: "variant fields get '@PARAM' tags on their parent enumerator" is the why and worth keeping. The rest ("we check for a matching tag and use its message as the field's comment") restates the
find/map.
The doc-comments on the new helper and on get_doc_comment_for_parameter are fine: they describe the API and the language rule, not the implementation. And deleting the old "3 steps" comment was the right call, same problem.
bernardnormier
left a comment
There was a problem hiding this comment.
One more on the helper, to finish the de-duplication.
| fn from(doc_comment: &GrammarDocComment) -> Self { | ||
| let overview = doc_comment.overview.as_ref().map(|message| { | ||
| message.value.iter().map(Into::into) | ||
| message.value.iter().map(Into::into).collect() |
There was a problem hiding this comment.
This is the same Message -> overview conversion that get_basic_doc_comment_from_message does. The helper can't be used here as-is because it returns a whole DocComment and this impl needs the bare overview plus real see_tags.
Suggest extracting the message conversion itself, e.g. fn convert_message(message: &slicec::grammar::Message) -> Message { message.value.iter().map(Into::into).collect() }, and calling it from both get_basic_doc_comment_from_message and here:
let overview = doc_comment.overview.as_ref().map(convert_message);There was a problem hiding this comment.
I'm not convinced this extra helper earns it's keep. It's a single-line of code, only called from 2 places.
I added it, and it actually increases our net-line-count by 3 lines.
I'd be fine with dropping your other helper (get_basic_doc_comment_from_message) in place of the convert_message suggestion. But if we added both helpers, then we'd have 2 helpers which are collectively only called by 3 places (that's together, not each). Only 1.5 calls per helper function is below the bar of usefulness IMO.
If the implementations were longer, I could see the point, but both helpers are basically one-liners.
|
Of the 5 doc-comments you flagged:
I feel the opposite here. The fact that single return types lack a name is, IMO, an obvious fact of the Slice language. |
This PR fixes #809, by ensuring that we detect and forward
@returnsfor return parameters, and@paramfor fields of enumerators. Previously, these were not detected at all, and were accidentally being thrown away.The fix for parameters was to add a
is_return: boolthat we check to know whether we're searching for@paramor@returns.We could split this into 2 functions instead of using a trailing bool; the function is small, and shares some code. But if anyone prefers 2 functions instead of my little bool, feel free to say so.
The fix for enumerator fields, is we re-use the logic for creating a struct field, but then 'patch' it's doc-comment with any
@paramthat were present on the enumerator.What's Changed
slicec:
Variantnow correctly have their doc-comments populated by@paramtags on their enclosing variant.returnTypefields of anOperationnow correctly have their doc-comments populated by@returnstags on their enclosing operation