[clang] [analyzer] Confidently inline final methods (PR #224070)
DonĂ¡t Nagy via cfe-commits
cfe-commits at lists.llvm.org
Thu Sep 17 06:05:25 PDT 2026
https://github.com/NagyDonat updated https://github.com/llvm/llvm-project/pull/224070
>From f437b536322b21820e962b8f45dd77204fbb9751 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Don=C3=A1t=20Nagy?= <donat.nagy at ericsson.com>
Date: Wed, 16 Sep 2026 15:26:00 +0200
Subject: [PATCH 1/7] Add failing reproducer from gh#222960
---
clang/test/Analysis/PR222960.cpp | 23 +++++++++++++++++++++++
1 file changed, 23 insertions(+)
create mode 100644 clang/test/Analysis/PR222960.cpp
diff --git a/clang/test/Analysis/PR222960.cpp b/clang/test/Analysis/PR222960.cpp
new file mode 100644
index 0000000000000..356594af424b9
--- /dev/null
+++ b/clang/test/Analysis/PR222960.cpp
@@ -0,0 +1,23 @@
+// RUN: %clang_analyze_cc1 -analyzer-checker=debug.ExprInspection -verify %s
+
+struct Msg
+{
+ virtual ~Msg() {}
+ virtual unsigned cmd() const = 0;
+};
+
+struct Ctrl : Msg
+{
+ unsigned c;
+ unsigned cmd() const final { return c; } // final: no override can exist
+};
+
+void clang_analyzer_dump(unsigned);
+void clang_analyzer_eval(bool);
+
+void test(Ctrl* p)
+{
+ clang_analyzer_dump(p->cmd());
+ // expected-warning-re at -1 {{reg_${{[0-9]+}}<unsigned int Element{SymRegion{reg_${{[0-9]+}}<Ctrl * p>},0 S64b,struct Ctrl}.c>}}
+ clang_analyzer_eval(p->cmd() == p->cmd()); // expected-warning {{TRUE}}
+}
>From 604aa82c591536da87ec80bc5cfe82b4f7a31ffc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Don=C3=A1t=20Nagy?= <donat.nagy at ericsson.com>
Date: Wed, 16 Sep 2026 15:29:26 +0200
Subject: [PATCH 2/7] Inline 'final' methods directly
---
clang/lib/StaticAnalyzer/Core/CallEvent.cpp | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/clang/lib/StaticAnalyzer/Core/CallEvent.cpp b/clang/lib/StaticAnalyzer/Core/CallEvent.cpp
index 2338c06d5f992..1ab1852507e8b 100644
--- a/clang/lib/StaticAnalyzer/Core/CallEvent.cpp
+++ b/clang/lib/StaticAnalyzer/Core/CallEvent.cpp
@@ -798,6 +798,10 @@ RuntimeDefinition CXXInstanceCall::getRuntimeDefinition() const {
if (!MD->isVirtual())
return AnyFunctionCall::getRuntimeDefinition();
+ // If the method is final or declared in a final class, we can inline it.
+ if (MD->hasAttr<FinalAttr>() || MD->getParent()->hasAttr<FinalAttr>())
+ return AnyFunctionCall::getRuntimeDefinition();
+
auto [RD, CanBeSubClass] = getDeclForDynamicType();
if (!RD || !RD->hasDefinition())
return {};
>From 58b39aeb666d3a374bfbfd4f01d9be18c49b6565 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Don=C3=A1t=20Nagy?= <donat.nagy at ericsson.com>
Date: Wed, 16 Sep 2026 17:43:26 +0200
Subject: [PATCH 3/7] Add more tests
---
clang/test/Analysis/PR222960.cpp | 23 ----
.../test/Analysis/inlining-final-methods.cpp | 126 ++++++++++++++++++
2 files changed, 126 insertions(+), 23 deletions(-)
delete mode 100644 clang/test/Analysis/PR222960.cpp
create mode 100644 clang/test/Analysis/inlining-final-methods.cpp
diff --git a/clang/test/Analysis/PR222960.cpp b/clang/test/Analysis/PR222960.cpp
deleted file mode 100644
index 356594af424b9..0000000000000
--- a/clang/test/Analysis/PR222960.cpp
+++ /dev/null
@@ -1,23 +0,0 @@
-// RUN: %clang_analyze_cc1 -analyzer-checker=debug.ExprInspection -verify %s
-
-struct Msg
-{
- virtual ~Msg() {}
- virtual unsigned cmd() const = 0;
-};
-
-struct Ctrl : Msg
-{
- unsigned c;
- unsigned cmd() const final { return c; } // final: no override can exist
-};
-
-void clang_analyzer_dump(unsigned);
-void clang_analyzer_eval(bool);
-
-void test(Ctrl* p)
-{
- clang_analyzer_dump(p->cmd());
- // expected-warning-re at -1 {{reg_${{[0-9]+}}<unsigned int Element{SymRegion{reg_${{[0-9]+}}<Ctrl * p>},0 S64b,struct Ctrl}.c>}}
- clang_analyzer_eval(p->cmd() == p->cmd()); // expected-warning {{TRUE}}
-}
diff --git a/clang/test/Analysis/inlining-final-methods.cpp b/clang/test/Analysis/inlining-final-methods.cpp
new file mode 100644
index 0000000000000..689cffed97f5c
--- /dev/null
+++ b/clang/test/Analysis/inlining-final-methods.cpp
@@ -0,0 +1,126 @@
+// RUN: %clang_analyze_cc1 -analyzer-checker=debug.ExprInspection -verify %s
+
+void clang_analyzer_dump(unsigned);
+void clang_analyzer_eval(bool);
+void clang_analyzer_warnIfReached();
+
+struct Msg {
+ virtual unsigned cmd() const = 0;
+};
+
+namespace gh222960 {
+// Ctrl::cmd() is final, the analyzer should not split off a "maybe dynamic
+// dispatch invokes a different overriding method" execution path, and only
+// follow the path where the method body is inlined.
+struct Ctrl : Msg {
+ unsigned c;
+ unsigned cmd() const final { return c; }
+};
+
+void test(Ctrl* p) {
+ clang_analyzer_dump(p->cmd());
+ // expected-warning-re at -1 {{reg_${{[0-9]+}}<unsigned int Element{SymRegion{reg_${{[0-9]+}}<Ctrl * p>},0 S64b,struct {{[0-9A-Za-z_]+}}::Ctrl}.c>}}
+ clang_analyzer_eval(p->cmd() == p->cmd()); // expected-warning {{TRUE}}
+}
+} // namespace gh222960
+
+namespace final_struct {
+// The analyzer should also confidently inline the method of a final class.
+struct Ctrl final : Msg {
+ unsigned c;
+ unsigned cmd() const override { return c; }
+};
+
+void test(Ctrl* p)
+{
+ clang_analyzer_dump(p->cmd());
+ // expected-warning-re at -1 {{reg_${{[0-9]+}}<unsigned int Element{SymRegion{reg_${{[0-9]+}}<Ctrl * p>},0 S64b,struct {{[0-9A-Za-z_]+}}::Ctrl}.c>}}
+ clang_analyzer_eval(p->cmd() == p->cmd()); // expected-warning {{TRUE}}
+}
+} // namespace final_class
+
+namespace final_method_on_child_ptr {
+// A final method should also be inlined when it is called through a pointer
+// whose (static) type is a child of the class where it was defined.
+struct Ctrl : Msg {
+ unsigned c;
+ unsigned cmd() const final { return c; }
+};
+
+struct Child : Ctrl {};
+
+void test(Child* p) {
+ clang_analyzer_dump(p->cmd());
+ // expected-warning-re at -1 {{reg_${{[0-9]+}}<unsigned int Base{SymRegion{reg_${{[0-9]+}}<Child * p>},Ctrl}.c>}}
+ clang_analyzer_eval(p->cmd() == p->cmd()); // expected-warning {{TRUE}}
+}
+} // namespace final_method_on_child_ptr
+
+namespace final_method_on_ptr_with_dyn_type_child {
+// A final method should also be inlined when it is called through a pointer
+// whose dynamic type is a child of the class where it was defined.
+struct Ctrl : Msg {
+ unsigned c;
+ unsigned cmd() const final { return c; }
+};
+
+struct Child : Ctrl {};
+
+void test(Ctrl* p) {
+ clang_analyzer_dump(p->cmd());
+ // expected-warning-re at -1 {{reg_${{[0-9]+}}<unsigned int Base{SymRegion{reg_${{[0-9]+}}<Child * p>},Ctrl}.c>}}
+ clang_analyzer_warnIfReached(); // expected-warning {{REACHABLE}}
+ clang_analyzer_eval(p->cmd() == p->cmd());
+ // FIXME: For unclear reasons, this clang_analyzer_eval call is not reached.
+}
+
+void entrypoint(Child *p) {
+ test(p);
+}
+} // namespace final_method_on_ptr_with_dyn_type_child
+
+namespace final_method_on_base_ptr_with_known_dyn_type {
+// A final method should also be inlined when it is called through a pointer
+// whose dynamic type is a child of the class where it was defined.
+// FIXME: This is not yet implemented, 'final' is only checked on the method
+// and class declaration corresponding to the static type of the pointee.
+struct Base : Msg {};
+
+struct Ctrl : Base {
+ unsigned c;
+ unsigned cmd() const final { return c; }
+};
+
+void test(Base* p) {
+ clang_analyzer_dump(p->cmd());
+ // expected-warning-re at -1 {{reg_${{[0-9]+}}<unsigned int Element{SymRegion{reg_${{[0-9]+}}<Ctrl * p>},0 S64b,struct {{[0-9A-Za-z_]+}}::Ctrl}.c>}}
+ // expected-warning at -2 {{conj_$}}
+ clang_analyzer_warnIfReached(); // expected-warning {{REACHABLE}}
+ clang_analyzer_eval(p->cmd() == p->cmd());
+ // FIXME: For unclear reasons, this clang_analyzer_eval call is not reached.
+}
+
+void entrypoint(Ctrl *p) {
+ test(p);
+}
+} // namespace final_method_on_base_ptr_with_known_dyn_type
+
+namespace nonfinal_bifurcates {
+// When the method is non-final and the dynamic type is unclear, the analysis
+// should bifurcate, with one branch inlining the method and the other branch
+// doing a conservative evaluation (which represents that another overriding
+// method is called).
+struct Ctrl : Msg {
+ unsigned c;
+ unsigned cmd() const override { return c; }
+};
+
+void test(Ctrl* p) {
+ clang_analyzer_dump(p->cmd());
+ // expected-warning-re at -1 {{reg_${{[0-9]+}}<unsigned int Element{SymRegion{reg_${{[0-9]+}}<Ctrl * p>},0 S64b,struct {{[0-9A-Za-z_]+}}::Ctrl}.c>}}
+ // expected-warning at -2 {{conj_$}}
+ clang_analyzer_eval(p->cmd() == p->cmd());
+ // expected-warning at -1 {{TRUE}}
+ // expected-warning at -2 {{FALSE}}
+}
+} // namespace nonfinal_bifurcate
>From 2841f4209f1d8ecb9a2f909198b23e169a9dc4e6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Don=C3=A1t=20Nagy?= <donat.nagy at ericsson.com>
Date: Wed, 16 Sep 2026 18:18:21 +0200
Subject: [PATCH 4/7] Also cover final methods of the dynamic type
---
clang/lib/StaticAnalyzer/Core/CallEvent.cpp | 5 +++++
clang/test/Analysis/inlining-final-methods.cpp | 3 ---
2 files changed, 5 insertions(+), 3 deletions(-)
diff --git a/clang/lib/StaticAnalyzer/Core/CallEvent.cpp b/clang/lib/StaticAnalyzer/Core/CallEvent.cpp
index 1ab1852507e8b..6598750c27ebf 100644
--- a/clang/lib/StaticAnalyzer/Core/CallEvent.cpp
+++ b/clang/lib/StaticAnalyzer/Core/CallEvent.cpp
@@ -826,6 +826,11 @@ RuntimeDefinition CXXInstanceCall::getRuntimeDefinition() const {
return {};
}
+ // A final method or a method of a final class cannot be overriden in a
+ // subclass.
+ if (Result->hasAttr<FinalAttr>() || Result->getParent()->hasAttr<FinalAttr>())
+ CanBeSubClass = false;
+
// Does the decl that we found have an implementation?
const FunctionDecl *Definition;
if (!Result->hasBody(Definition)) {
diff --git a/clang/test/Analysis/inlining-final-methods.cpp b/clang/test/Analysis/inlining-final-methods.cpp
index 689cffed97f5c..f8cf2efdf403a 100644
--- a/clang/test/Analysis/inlining-final-methods.cpp
+++ b/clang/test/Analysis/inlining-final-methods.cpp
@@ -82,8 +82,6 @@ void entrypoint(Child *p) {
namespace final_method_on_base_ptr_with_known_dyn_type {
// A final method should also be inlined when it is called through a pointer
// whose dynamic type is a child of the class where it was defined.
-// FIXME: This is not yet implemented, 'final' is only checked on the method
-// and class declaration corresponding to the static type of the pointee.
struct Base : Msg {};
struct Ctrl : Base {
@@ -94,7 +92,6 @@ struct Ctrl : Base {
void test(Base* p) {
clang_analyzer_dump(p->cmd());
// expected-warning-re at -1 {{reg_${{[0-9]+}}<unsigned int Element{SymRegion{reg_${{[0-9]+}}<Ctrl * p>},0 S64b,struct {{[0-9A-Za-z_]+}}::Ctrl}.c>}}
- // expected-warning at -2 {{conj_$}}
clang_analyzer_warnIfReached(); // expected-warning {{REACHABLE}}
clang_analyzer_eval(p->cmd() == p->cmd());
// FIXME: For unclear reasons, this clang_analyzer_eval call is not reached.
>From b729d8fb5a60a2b7e1690d00aa56023521548033 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Don=C3=A1t=20Nagy?= <donat.nagy at ericsson.com>
Date: Wed, 16 Sep 2026 20:36:24 +0200
Subject: [PATCH 5/7] Also cover inherited nonfinal method on an object with a
final type
---
clang/lib/StaticAnalyzer/Core/CallEvent.cpp | 7 ++-
.../test/Analysis/inlining-final-methods.cpp | 43 ++++++++++++++++++-
2 files changed, 47 insertions(+), 3 deletions(-)
diff --git a/clang/lib/StaticAnalyzer/Core/CallEvent.cpp b/clang/lib/StaticAnalyzer/Core/CallEvent.cpp
index 6598750c27ebf..58e0ce636da53 100644
--- a/clang/lib/StaticAnalyzer/Core/CallEvent.cpp
+++ b/clang/lib/StaticAnalyzer/Core/CallEvent.cpp
@@ -806,6 +806,10 @@ RuntimeDefinition CXXInstanceCall::getRuntimeDefinition() const {
if (!RD || !RD->hasDefinition())
return {};
+ // We can confidently inline a method called on an object with final type.
+ if (RD->hasAttr<FinalAttr>())
+ CanBeSubClass = false;
+
// Find the decl for this method in that class.
const CXXMethodDecl *Result = MD->getCorrespondingMethodInClass(RD, true);
if (!Result) {
@@ -826,8 +830,7 @@ RuntimeDefinition CXXInstanceCall::getRuntimeDefinition() const {
return {};
}
- // A final method or a method of a final class cannot be overriden in a
- // subclass.
+ // A final method cannot be overriden in a subclass.
if (Result->hasAttr<FinalAttr>() || Result->getParent()->hasAttr<FinalAttr>())
CanBeSubClass = false;
diff --git a/clang/test/Analysis/inlining-final-methods.cpp b/clang/test/Analysis/inlining-final-methods.cpp
index f8cf2efdf403a..abd916fb79454 100644
--- a/clang/test/Analysis/inlining-final-methods.cpp
+++ b/clang/test/Analysis/inlining-final-methods.cpp
@@ -56,6 +56,24 @@ void test(Child* p) {
}
} // namespace final_method_on_child_ptr
+namespace nonfinal_method_on_final_child_ptr {
+// We can confidently inline even a non-final method of a non-final class if it
+// is called on an object whose type is final and does not override it.
+struct Ctrl : Msg {
+ unsigned c;
+ unsigned cmd() const override { return c; }
+};
+
+struct Child final : Ctrl {};
+
+void test(Child* p) {
+ clang_analyzer_dump(p->cmd());
+ // expected-warning-re at -1 {{reg_${{[0-9]+}}<unsigned int Base{SymRegion{reg_${{[0-9]+}}<Child * p>},Ctrl}.c>}}
+ clang_analyzer_eval(p->cmd() == p->cmd()); // expected-warning {{TRUE}}
+}
+} // namespace nonfinal_method_on_final_child_ptr
+
+
namespace final_method_on_ptr_with_dyn_type_child {
// A final method should also be inlined when it is called through a pointer
// whose dynamic type is a child of the class where it was defined.
@@ -102,6 +120,29 @@ void entrypoint(Ctrl *p) {
}
} // namespace final_method_on_base_ptr_with_known_dyn_type
+namespace nonfinal_method_on_ptr_with_dyn_type_final {
+// We can confidently inline even a non-final method of a non-final class if it
+// is called on an object whose dynamic type is final and does not override it.
+struct Ctrl : Msg {
+ unsigned c;
+ unsigned cmd() const override { return c; }
+};
+
+struct Child final : Ctrl {};
+
+void test(Ctrl* p) {
+ clang_analyzer_dump(p->cmd());
+ // expected-warning-re at -1 {{reg_${{[0-9]+}}<unsigned int Base{SymRegion{reg_${{[0-9]+}}<Child * p>},Ctrl}.c>}}
+ clang_analyzer_warnIfReached(); // expected-warning {{REACHABLE}}
+ clang_analyzer_eval(p->cmd() == p->cmd());
+ // FIXME: For unclear reasons, this clang_analyzer_eval call is not reached.
+}
+
+void entrypoint(Child *p) {
+ test(p);
+}
+} // namespace nonfinal_method_on_ptr_with_dyn_type_final
+
namespace nonfinal_bifurcates {
// When the method is non-final and the dynamic type is unclear, the analysis
// should bifurcate, with one branch inlining the method and the other branch
@@ -120,4 +161,4 @@ void test(Ctrl* p) {
// expected-warning at -1 {{TRUE}}
// expected-warning at -2 {{FALSE}}
}
-} // namespace nonfinal_bifurcate
+} // namespace nonfinal_bifurcates
>From e7bb1015eb0315edce0d1153a089e4bee784f942 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Don=C3=A1t=20Nagy?= <donat.nagy at ericsson.com>
Date: Thu, 17 Sep 2026 15:03:55 +0200
Subject: [PATCH 6/7] Workaround for clang_analyzer_eval quirk
Apparently it does not emit anything when it is encountered in an
inlined function.
---
.../test/Analysis/inlining-final-methods.cpp | 37 ++++++-------------
1 file changed, 12 insertions(+), 25 deletions(-)
diff --git a/clang/test/Analysis/inlining-final-methods.cpp b/clang/test/Analysis/inlining-final-methods.cpp
index abd916fb79454..3f86700558ed9 100644
--- a/clang/test/Analysis/inlining-final-methods.cpp
+++ b/clang/test/Analysis/inlining-final-methods.cpp
@@ -2,7 +2,6 @@
void clang_analyzer_dump(unsigned);
void clang_analyzer_eval(bool);
-void clang_analyzer_warnIfReached();
struct Msg {
virtual unsigned cmd() const = 0;
@@ -84,16 +83,12 @@ struct Ctrl : Msg {
struct Child : Ctrl {};
-void test(Ctrl* p) {
+void test(Child* childp) {
+ Ctrl *p = childp;
clang_analyzer_dump(p->cmd());
- // expected-warning-re at -1 {{reg_${{[0-9]+}}<unsigned int Base{SymRegion{reg_${{[0-9]+}}<Child * p>},Ctrl}.c>}}
- clang_analyzer_warnIfReached(); // expected-warning {{REACHABLE}}
+ // expected-warning-re at -1 {{reg_${{[0-9]+}}<unsigned int Base{SymRegion{reg_${{[0-9]+}}<Child * childp>},Ctrl}.c>}}
clang_analyzer_eval(p->cmd() == p->cmd());
- // FIXME: For unclear reasons, this clang_analyzer_eval call is not reached.
-}
-
-void entrypoint(Child *p) {
- test(p);
+ // expected-warning at -1 {{TRUE}}
}
} // namespace final_method_on_ptr_with_dyn_type_child
@@ -107,16 +102,12 @@ struct Ctrl : Base {
unsigned cmd() const final { return c; }
};
-void test(Base* p) {
+void test(Ctrl* ctrlp) {
+ Base *p = ctrlp;
clang_analyzer_dump(p->cmd());
- // expected-warning-re at -1 {{reg_${{[0-9]+}}<unsigned int Element{SymRegion{reg_${{[0-9]+}}<Ctrl * p>},0 S64b,struct {{[0-9A-Za-z_]+}}::Ctrl}.c>}}
- clang_analyzer_warnIfReached(); // expected-warning {{REACHABLE}}
+ // expected-warning-re at -1 {{reg_${{[0-9]+}}<unsigned int Element{SymRegion{reg_${{[0-9]+}}<Ctrl * ctrlp>},0 S64b,struct {{[0-9A-Za-z_]+}}::Ctrl}.c>}}
clang_analyzer_eval(p->cmd() == p->cmd());
- // FIXME: For unclear reasons, this clang_analyzer_eval call is not reached.
-}
-
-void entrypoint(Ctrl *p) {
- test(p);
+ // expected-warning at -1 {{TRUE}}
}
} // namespace final_method_on_base_ptr_with_known_dyn_type
@@ -130,16 +121,12 @@ struct Ctrl : Msg {
struct Child final : Ctrl {};
-void test(Ctrl* p) {
+void test(Child* childp) {
+ Ctrl *p = childp;
clang_analyzer_dump(p->cmd());
- // expected-warning-re at -1 {{reg_${{[0-9]+}}<unsigned int Base{SymRegion{reg_${{[0-9]+}}<Child * p>},Ctrl}.c>}}
- clang_analyzer_warnIfReached(); // expected-warning {{REACHABLE}}
+ // expected-warning-re at -1 {{reg_${{[0-9]+}}<unsigned int Base{SymRegion{reg_${{[0-9]+}}<Child * childp>},Ctrl}.c>}}
clang_analyzer_eval(p->cmd() == p->cmd());
- // FIXME: For unclear reasons, this clang_analyzer_eval call is not reached.
-}
-
-void entrypoint(Child *p) {
- test(p);
+ // expected-warning at -1 {{TRUE}}
}
} // namespace nonfinal_method_on_ptr_with_dyn_type_final
>From db70b1b4189560f01c9af886b83acc4ccf69a179 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Don=C3=A1t=20Nagy?= <donat.nagy at ericsson.com>
Date: Thu, 17 Sep 2026 15:05:06 +0200
Subject: [PATCH 7/7] Fix an end namespace comment in a test
---
clang/test/Analysis/inlining-final-methods.cpp | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/clang/test/Analysis/inlining-final-methods.cpp b/clang/test/Analysis/inlining-final-methods.cpp
index 3f86700558ed9..0a8e1653d64c8 100644
--- a/clang/test/Analysis/inlining-final-methods.cpp
+++ b/clang/test/Analysis/inlining-final-methods.cpp
@@ -36,7 +36,7 @@ void test(Ctrl* p)
// expected-warning-re at -1 {{reg_${{[0-9]+}}<unsigned int Element{SymRegion{reg_${{[0-9]+}}<Ctrl * p>},0 S64b,struct {{[0-9A-Za-z_]+}}::Ctrl}.c>}}
clang_analyzer_eval(p->cmd() == p->cmd()); // expected-warning {{TRUE}}
}
-} // namespace final_class
+} // namespace final_struct
namespace final_method_on_child_ptr {
// A final method should also be inlined when it is called through a pointer
More information about the cfe-commits
mailing list