[clang-tidy] Add `modernize-use-contracts` check
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
It would be useful to have a clang-tidy check that helps migrate existing assertion-based code toward C++ contracts.
A reasonable initial scope for such a check would be:
- rewrite `assert(expr);` as `contract_assert(expr);`
- promote obvious leading assertions in a function body to `pre(...)` conditions on the function declaration, when those assertions clearly describe requirements on parameters or `this`
The check should be conservative. If an assertion is not clearly a precondition, it should remain in the function body and only be rewritten as `contract_assert(...)`.
For example:
```cpp
size_t encode_packet(std::span payload,
std::span output,
bool include_checksum) {
assert(output.data() != nullptr);
assert(payload.data() != nullptr || payload.empty());
assert(output.size() >= header_size);
assert(!include_checksum || output.size() >= header_size + checksum_size);
size_t pos = 0;
pos += write_header(output.subspan(pos), payload.size(), include_checksum);
assert(pos <= output.size());
pos += write_payload(output.subspan(pos), payload);
if (include_checksum) {
auto checksum = compute_checksum(payload);
pos += write_checksum(output.subspan(pos), checksum);
}
assert(pos <= output.size());
assert(pos >= header_size);
return pos;
}
```
An initial implementation could reasonably produce something like:
```cpp
size_t encode_packet(std::span payload,
std::span output,
bool include_checksum)
pre(output.data() != nullptr)
pre(payload.data() != nullptr || payload.empty())
pre(output.size() >= header_size)
pre(!include_checksum || output.size() >= header_size + checksum_size)
{
size_t pos = 0;
pos += write_header(output.subspan(pos), payload.size(), include_checksum);
contract_assert(pos <= output.size());
pos += write_payload(output.subspan(pos), payload);
if (include_checksum) {
auto checksum = compute_checksum(payload);
pos += write_checksum(output.subspan(pos), checksum);
}
contract_assert(pos <= output.size());
contract_assert(pos >= header_size);
return pos;
}
```
This keeps the first version focused on transformations that are straightforward and low-risk:
- leading input checks become `pre(...)`
- internal assertions become `contract_assert(...)`
A more ambitious future extension could consider postconditions. In this example, the final assertions are suggestive of result constraints and might ideally become:
```cpp
size_t encode_packet(std::span payload,
std::span output,
bool include_checksum)
pre(output.data() != nullptr)
pre(payload.data() != nullptr || payload.empty())
pre(output.size() >= header_size)
pre(!include_checksum || output.size() >= header_size + checksum_size)
post(r: r <= output.size())
post(r: r >= header_size)
{
size_t pos = 0;
pos += write_header(output.subspan(pos), payload.size(), include_checksum);
contract_assert(pos <= output.size());
pos += write_payload(output.subspan(pos), payload);
if (include_checksum) {
auto checksum = compute_checksum(payload);
pos += write_checksum(output.subspan(pos), checksum);
}
return pos;
}
```
However, inferring `post(...)` conditions safely is likely harder than handling `assert` to `contract_assert` and obvious `pre(...)` promotion. Because of that, it seems best to keep postconditions out of the initial implementation and treat them as future work or an open design question.
Contributor guide
Assessment
This issue has not been assessed yet.