-
Notifications
You must be signed in to change notification settings - Fork 605
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: add a style guide #1407
RFC: add a style guide #1407
Conversation
|
||
We use 3 blocks of imports in our Rust files: | ||
|
||
1. `std` imports |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Currently I believe rustls more often uses a crate
, external, std
/core
style, so we can decide which order we prefer. I believe the outside-in style described here is more idiomatic. There is some support in rustfmt and tooling like Rust-Analyzer for splitting up blocks like this.
Codecov Report
@@ Coverage Diff @@
## main #1407 +/- ##
=======================================
Coverage 96.43% 96.43%
=======================================
Files 72 72
Lines 15163 15163
=======================================
Hits 14622 14622
Misses 541 541 📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more |
One thing that's not described here but which we do in Quinn is a discussion of optimal module sizes. For Quinn, we like to target modules (files) in the 400-800 line category. To me, that feels like a good size where on the one hand, you don't end up jumping between many small files too much but on the other hand you don't end up searching through a large file a lot. |
CONTRIBUTING.md
Outdated
We believe (properly commented) large functions can be more readable than | ||
splitting it up into many small single-use functions. Single-caller functions | ||
can be useful, but consider whether the code would be easier to follow if the | ||
function was inlined (especially for short functions). |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hm, I don't feel fully on board with this still. Especially, I've been considering writing a PR which refactors src/msgs/deframer.rs
into a style where the comments that introduce sections of code are in individual singler-caller functions that can be understood incrementally, as opposed to a single 200-line one which has to be understood as a whole.
As an illustration as to why I think single-use functions are better than comments: there's a comment there which talks about "place the message onto the frames output queue", but there is no such queue now. If there was a single-use function like fn deframe_one_message_if_present(output: &mut VecDequeue<Message>)
(or whatever) the compiler would be involved in ensuring the structural documentation (the names, arguments, returns of the smaller functions) made sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I agree the deframer is pretty gnarly, although I think it's not very representative of the larger point (with its nested loops). This part of the style guide tries to thread the needle of making recommendations for the common case without imposing strict rules -- I know there have certainly been other areas where I did go for single-use functions to make the high-level flow clearer. Do you want to try to reword this into something that you'd feel more comfortable with?
For your specific example, it sounds like that comment might just be outdated, which might be a separate issue -- but I'm also curious how you would structure MessageDeframer::pop()
. It can also sometimes be tricky to get the borrow checker to handle splitting up code like this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a discussion point, consider something more like server::tls13::client_hello::handle_client_hello()
. Do you also feel that should get split up into smaller functions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you also feel that should get split up into smaller functions?
Yes, I think so. A prime candidate there would be the block around where resumedata
needs to be mut
. If all that was in a function that fully decides on resumedata
/chosen_psk_index
, I think it would be clearer.
I don't think I'd extract more than one or two functions from there though -- I think it's beneficial for that function if it contains all the possible transitions to subsequent states in the function itself. But quite a lot of it is of the form if
client_hello.something().has_some_required_property() { return Err(cx.common.send_fatal_alert(...)); }`.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's fair, I think both the chosen_share == None
and psk
handling are kinda clear candidates here. So do you think we should just strike this language, or will it still be useful if weakened form?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
or will it still be useful if weakened form?
I don't have a strong opinion but I think there's still utility in a weakened form.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's a somewhat softer take, what do you think?
"While single-use functions can make sense if the algorithm is sufficiently complex that it warrants an explicit name and interface, using many short single-use functions can make the code harder to follow, due to having to jump around in order to gain an understanding of what's going on. When writing a single-use function, consider whether it needs the dedicated interface, or if it could be inlined into its caller instead."
Thanks for writing this down! |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for putting this together @djc, it's really nice to have this written out in one place. It's a great document for newcomers to the project (or Rust generally).
I've also been collecting up a list of things I've picked up from code review over the past few months. Here are some suggestions to consider (in no particular order). Some might be too specific but I think a few are worth rolling in to this document:
- Avoid type annotations where possible, and when required, prefer using the turbofish
(This might be controversial, e.g. see this comment from @jsha on a rustls-ffi PR where he argued we should prefer the opposite approach). - Avoid free-standing functions, prefer using associated methods.
- Avoid
ref
in match expressions, prefer taking a reference on the scruntinee. - Avoid
validate
type methods that could be forgotten, prefer "parse, don't validate" style. - Hoist
Ok
andErr
to the outside of amatch
where possible, instead of duplicating in the match arms. - Don't elide lifetimes, always include
<'_>
to avoid confusion. - Use
impl ...
in arguments where possible to avoid generic type argument indirection that's harder to read in one pass. - Prefer expanding acronyms in function names, arguments etc (e.g.
keyusage
vsku
) and avoid shorthands (e.g.anon
vsanonymous
). - Avoid type aliases since they can obfuscate the underlying type without providing significant additional type safety.
- Prefer hex for numeric literals as opposed to decimal.
- Write
enum
variants in alphabetic order. - Prefer
enum
variants that are active verbs (e.g.Allow
vsAllowed
).
13 Import core traits (e.g. useTryFrom
vscore::convert::TryFrom
).
WDYT?
Edited this one. I don't think the intention is to avoid lifetime elision in all places, just in ones where there's type generic over a lifetime. That matches what the rust reference says: https://doc.rust-lang.org/reference/lifetime-elision.html (the line ending "elided - preferred". I think we have |
Not sure what you mean by "edited", is this coming to a force push in the future? My intent here was definitely to avoid lifetime elision in paths (that is, type names) to use the placeholder lifetime instead of not mentioning any lifetimes at all -- so I think we're in agreement. |
Sorry, I had quoted and edited @cpu's item 6.
Yep! |
Ahh, awesome list of @djc's greatest nits. I'll see if I can splice these in. |
😆 Thanks! |
@djc is this branch still on your radar? Would it be helpful if I tried integrating my big-list-of-nits into the text? |
Yes, that would definitely be helpful! It is still on my radar, but so are a bunch of other things... |
Sounds good. I will make a PR targeting this branch so you can review the additions. |
Up for review in #1461 |
Rebased this on main. |
The existing text has fallen out of sync with `SECURITY.md` and recommends sending security issues through regular GitHub issue, or email to Ctz. This commit updates the text to match what's in the up-to-date `SECURITY.md`: use the GitHub security advisory tooling. That's what it's made for.
Thanks again, this is a nice update 📚 |
This is based on a style guide from an existing private repository which I wrote last year. A lot of this is based on a style that I've refined with @Ralith while working on Quinn.