Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions build/tools/clang_tidy/clang_tidy.bazelrc
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,7 @@ build:clang-tidy-warnings --config=clang-tidy --aspects_parameters=clang_tidy_ar
# Unsafe continuation captures: flags bare references, raw pointers, and
# non-owning views captured into lambdas passed to async sinks.
build:clang-tidy-unsafe-continuation-capture --config=clang-tidy --aspects_parameters=clang_tidy_args=--checks=workerd-unsafe-continuation-capture

# Lambdas capturing `this` in constructor initializer lists, before the object
# is fully constructed. Not yet clean on existing code.
build:clang-tidy-ctor-initializer-this-capture --config=clang-tidy --aspects_parameters=clang_tidy_args=--checks=workerd-ctor-initializer-this-capture
21 changes: 21 additions & 0 deletions tools/clang-tidy/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
WD_CONSUME are made through consume() when invoked on kj::Ptr.
- `workerd-coroutine-hostile-raii`: flags hostile RAII objects that persist
across coroutine suspension.
- `workerd-ctor-initializer-this-capture`: flags lambdas that capture `this`
in a constructor's mem-initializer list, before the object is fully
constructed.
- `workerd-legacy-stream-alloc`: flags direct allocation of the legacy C++
ReadableStream / WritableStream (`js.alloc<ReadableStream>(...)`) outside
JsReadableStream::create() / JsWritableStream::create(), the dispatch
Expand Down Expand Up @@ -44,6 +47,7 @@ filegroup(
"angled-includes.c++",
"consume.c++",
"coroutine-hostile-raii.c++",
"ctor-initializer-this-capture.c++",
"legacy-stream-alloc.c++",
"promise-ignore-result.c++",
"unsafe-continuation-capture.c++",
Expand All @@ -61,6 +65,7 @@ filegroup(
"angled-includes.h",
"consume.h",
"coroutine-hostile-raii.h",
"ctor-initializer-this-capture.h",
"legacy-stream-alloc.h",
"promise-ignore-result.h",
"unsafe-continuation-capture.h",
Expand Down Expand Up @@ -164,6 +169,22 @@ sh_test(
],
)

sh_test(
name = "ctor-initializer-this-capture-test",
srcs = ["ctor-initializer-this-capture-test.sh"],
data = [
":ctor-initializer-this-capture-negative-test.c++",
":ctor-initializer-this-capture-positive-test.c++",
":workerd-lint",
"//tools:clang-tidy",
],
tags = ["no-asan"],
target_compatible_with = [
"@platforms//os:linux",
"@platforms//cpu:x86_64",
],
)

sh_test(
name = "legacy-stream-alloc-test",
srcs = ["legacy-stream-alloc-test.sh"],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// Copyright (c) 2017-2026 Cloudflare, Inc.
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
// https://opensource.org/licenses/Apache-2.0

// Negative fixtures for workerd-ctor-initializer-this-capture: none of these
// may produce a diagnostic.

namespace fixtures {

class Callback {
public:
Callback() = default;
template <typename F>
Callback(F&& f) {} // NOLINT(google-explicit-constructor)
};

template <typename F>
int compute(F&& f) {
return 0;
}

class Widget {
public:
// Case N1: the lambda is created in the constructor body, after every
// member has been initialized.
explicit Widget(int) {
onEvent = [this]() { handle(); };
}

// Case N2: lambdas that capture nothing, or only constructor parameters.
Widget(int a, int& b): onEvent([a, &b]() { b += a; }), other([] {}), value(a) {}

// Case N3: an immediately invoked lambda does not outlive the initializer.
explicit Widget(long v)
: onEvent([] {}),
other([] {}),
value([&]() { return v > 0 ? first : static_cast<int>(v); }()) {}

// Case N4: same, with an explicit `this` capture.
explicit Widget(short v): onEvent([] {}), other([] {}), value([this]() { return first; }()) {}

// Case N5: a lambda passed to a function that captures only parameters.
explicit Widget(char v): onEvent([] {}), other([] {}), value(compute([v]() { return v; })) {}

// Case N6: explicit suppression.
explicit Widget(bool)
: onEvent([this]() { handle(); }), // NOLINT(workerd-ctor-initializer-this-capture)
other([] {}),
value(0) {}

void handle() {
// Case N7: capturing `this` in an ordinary member function.
onEvent = [this]() { handle(); };
}

private:
int first = 0;
// Case N8: default member initializers are not checked.
Callback onEvent = [this]() { handle(); };
Callback other;
int value;
};

// Case N9: a lambda in a constructor body of a class nested in another
// constructor's initializer is not part of that initializer's object.
class Outer {
public:
Outer()
: callback([]() {
struct Local {
Local() {
cb = [this]() {};
}
Callback cb;
};
Local local;
}) {}

private:
Callback callback;
};

} // namespace fixtures
159 changes: 159 additions & 0 deletions tools/clang-tidy/ctor-initializer-this-capture-positive-test.c++
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
// Copyright (c) 2017-2026 Cloudflare, Inc.
// Licensed under the Apache 2.0 license found in the LICENSE file or at:
// https://opensource.org/licenses/Apache-2.0

// Positive fixtures for workerd-ctor-initializer-this-capture: every case must
// produce exactly one diagnostic.

namespace fixtures {

// Type-erased stand-in for kj::Function: stores a callable.
class Callback {
public:
template <typename F>
Callback(F&& f) {} // NOLINT(google-explicit-constructor)
Callback(const Callback&) = default;
};

class Promise {
public:
template <typename F>
Promise then(F&& f) {
return *this;
}
};

Promise start();

template <typename F>
Callback wrap(F&& f) {
return Callback(static_cast<F&&>(f));
}

class Base {
public:
explicit Base(Callback callback): callback(callback) {}

private:
Callback callback;
};

class Widget: public Base {
public:
// Case P1: explicit `this` capture.
explicit Widget(int): Base([] {}), onEvent([this]() { handle(); }), task(start()), value(0) {}

// Case P2: `*this` copies the partially constructed object.
explicit Widget(long)
: Base([] {}),
onEvent([*this]() mutable { handle(); }),
task(start()),
value(0) {}

// Case P3: implicit capture through `[&]`.
explicit Widget(short): Base([] {}), onEvent([&]() { handle(); }), task(start()), value(0) {}

// Case P4: implicit capture through `[=]`.
explicit Widget(char): Base([] {}), onEvent([=]() { handle(); }), task(start()), value(0) {}

// Case P5: base-class initializer.
explicit Widget(float): Base([this]() { handle(); }), onEvent([] {}), task(start()), value(0) {}

// Case P6: delegating initializer.
explicit Widget(double): Widget(wrap([this]() { handle(); })) {}

// Case P7: init-capture of `this`.
explicit Widget(unsigned)
: Base([] {}),
onEvent([self = this]() { self->handle(); }),
task(start()),
value(0) {}

// Case P8: init-capture of a reference to `*this`.
explicit Widget(unsigned long)
: Base([] {}),
onEvent([&self = *this]() { self.handle(); }),
task(start()),
value(0) {}

// Case P9: lambda passed to a continuation inside a larger expression.
explicit Widget(unsigned short)
: Base([] {}),
onEvent([] {}),
task(start().then([this]() { handle(); })),
value(0) {}

// Case P10: lambda that escapes from an immediately invoked lambda.
explicit Widget(unsigned char)
: Base([] {}),
onEvent([&]() { return wrap([this]() { handle(); }); }()),
task(start()),
value(0) {}

// Case P11: lambda created by an init-capture of a lambda that does not
// itself capture `this`.
explicit Widget(bool)
: Base([] {}),
onEvent([inner = wrap([this]() { handle(); })]() {}),
task(start()),
value(0) {}

// Case P12: explicit `this` alongside a capture default.
Widget(int, int): Base([] {}), onEvent([&, this]() { handle(); }), task(start()), value(0) {}

void handle() {}

private:
explicit Widget(Callback callback): Base(callback), onEvent(callback), task(start()), value(0) {}

Callback onEvent;
Promise task;
int value;
};

// Case P13: constructor defined out of line.
class OutOfLine {
public:
OutOfLine();
void handle() {}

private:
Callback onEvent;
};

OutOfLine::OutOfLine(): onEvent([this]() { handle(); }) {}

// Case P14: class template, reported once despite two instantiations.
template <typename T>
class Holder {
public:
Holder(): onEvent([this]() { handle(); }) {}
void handle() {}

private:
Callback onEvent;
};

// Case P15: class template whose capture default captures `this` only through
// a dependent member access.
template <typename T>
class DependentHolder {
public:
DependentHolder(): onEvent([&]() { value.handle(); }) {}

private:
T value;
Callback onEvent;
};

struct Handler {
void handle() {}
};

void instantiate() {
Holder<int> a;
Holder<long> b;
DependentHolder<Handler> c;
}

} // namespace fixtures
Loading
Loading