llvm / llvm/llvm-project

[clang-tidy] Add `modernize-use-contracts` check

Open
#190,344 4 comments 0 reactions 0 assignees View on GitHub
c++26 check-request clang-tidy
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.