Skip to content

Conversation

MythreyaK
Copy link
Contributor

@MythreyaK MythreyaK commented Aug 24, 2025

Fixes clangd/clangd#2323.

Assumes self is of the record type in the declaration.

struct Foo {
  int member {};
  auto&& getter1(this auto&& self) { // assume `self` is is `Foo`
    return self.member;
};

Assume `self` parameter is of the parent record type
@MythreyaK MythreyaK force-pushed the mythreyak/this-auto-heuristic branch from 69ff01b to c1cdb39 Compare August 24, 2025 06:57
@MythreyaK MythreyaK marked this pull request as ready for review August 24, 2025 07:28
@llvmbot llvmbot added clang Clang issues not falling into any other category clang-tools-extra clangd clang:frontend Language frontend issues, e.g. anything involving "Sema" labels Aug 24, 2025
@llvmbot
Copy link
Member

llvmbot commented Aug 24, 2025

@llvm/pr-subscribers-clangd

@llvm/pr-subscribers-clang-tools-extra

Author: Mythreya Kuricheti (MythreyaK)

Changes

Fixes clangd/clangd#2323.

Assumes self is of the record type in the declaration.

struct Foo {
  int member {};
  auto&& getter1(this auto&& self) { // assume `self` is is `Foo`
    return self.member;
};

Full diff: https://github.com/llvm/llvm-project/pull/155143.diff

3 Files Affected:

  • (modified) clang-tools-extra/clangd/unittests/RenameTests.cpp (+42-1)
  • (modified) clang/lib/Sema/HeuristicResolver.cpp (+26)
  • (modified) clang/unittests/Sema/HeuristicResolverTest.cpp (+18-1)
diff --git a/clang-tools-extra/clangd/unittests/RenameTests.cpp b/clang-tools-extra/clangd/unittests/RenameTests.cpp
index 2cb0722f7f285..4701ae07d0d12 100644
--- a/clang-tools-extra/clangd/unittests/RenameTests.cpp
+++ b/clang-tools-extra/clangd/unittests/RenameTests.cpp
@@ -17,10 +17,11 @@
 #include "clang/Tooling/Core/Replacement.h"
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/Support/MemoryBuffer.h"
-#include <algorithm>
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 
+#include <algorithm>
+
 namespace clang {
 namespace clangd {
 namespace {
@@ -2468,6 +2469,46 @@ TEST(CrossFileRenameTests, adjustmentCost) {
   }
 }
 
+TEST(RenameTest, RenameWithExplicitObjectPararameter) {
+  Annotations Test = {R"cpp(
+    struct Foo {
+      int [[memb^er]] {};
+      auto&& getter1(this auto&& self) {
+        auto local = [&] {
+          return self.[[memb^er]];
+        }();
+        return local + self.[[memb^er]];
+      }
+      auto&& getter2(this Foo&& self) {
+        return self.[[memb^er]];
+      }
+      int normal() {
+        return [[memb^er]];
+      }
+    };
+  )cpp"};
+
+  auto TU = TestTU::withCode(Test.code());
+  TU.ExtraArgs.push_back("-std=c++23");
+  auto AST = TU.build();
+
+  llvm::StringRef NewName = "m_member";
+  auto Index = TU.index();
+
+  for (const auto &RenamePos : Test.points()) {
+    auto RenameResult = rename({RenamePos, NewName, AST, testPath(TU.Filename),
+                                getVFSFromAST(AST), Index.get()});
+
+    ASSERT_TRUE(bool(RenameResult)) << RenameResult.takeError();
+    auto Res = RenameResult.get();
+
+    ASSERT_TRUE(bool(RenameResult)) << RenameResult.takeError();
+    ASSERT_EQ(1u, RenameResult->GlobalChanges.size());
+    EXPECT_EQ(applyEdits(std::move(RenameResult->GlobalChanges)).front().second,
+              expectedResult(Test, NewName));
+  }
+}
+
 } // namespace
 } // namespace clangd
 } // namespace clang
diff --git a/clang/lib/Sema/HeuristicResolver.cpp b/clang/lib/Sema/HeuristicResolver.cpp
index 933841beeac3d..20f7d0fca2066 100644
--- a/clang/lib/Sema/HeuristicResolver.cpp
+++ b/clang/lib/Sema/HeuristicResolver.cpp
@@ -14,6 +14,7 @@
 #include "clang/AST/TemplateBase.h"
 #include "clang/AST/Type.h"
 #include "llvm/ADT/identity.h"
+#include <optional>
 
 namespace clang {
 
@@ -301,9 +302,34 @@ std::vector<const NamedDecl *> HeuristicResolverImpl::resolveMemberExpr(
     return {};
   }
 
+  // check if member expr is in the context of an explicit object method
+  // If so, it's safe to assume the templated arg is of type of the record
+  const auto ExplicitMemberHeuristic =
+      [&](const Expr *Base) -> std::optional<QualType> {
+    if (auto *DeclRef = dyn_cast_if_present<DeclRefExpr>(Base)) {
+      auto *PrDecl = dyn_cast_if_present<ParmVarDecl>(DeclRef->getDecl());
+
+      if (PrDecl && PrDecl->isExplicitObjectParameter()) {
+        auto CxxRecord = dyn_cast_if_present<CXXRecordDecl>(
+            PrDecl->getDeclContext()->getParent());
+
+        if (CxxRecord) {
+          return Ctx.getTypeDeclType(dyn_cast<TypeDecl>(CxxRecord));
+        }
+      }
+    }
+
+    return std::nullopt;
+  };
+
   // Try resolving the member inside the expression's base type.
   Expr *Base = ME->isImplicitAccess() ? nullptr : ME->getBase();
   QualType BaseType = ME->getBaseType();
+
+  if (auto Type = ExplicitMemberHeuristic(Base)) {
+    BaseType = *Type;
+  }
+
   BaseType = simplifyType(BaseType, Base, ME->isArrow());
   return resolveDependentMember(BaseType, ME->getMember(), NoFilter);
 }
diff --git a/clang/unittests/Sema/HeuristicResolverTest.cpp b/clang/unittests/Sema/HeuristicResolverTest.cpp
index 7df25e01e66d4..21aca7a3489b8 100644
--- a/clang/unittests/Sema/HeuristicResolverTest.cpp
+++ b/clang/unittests/Sema/HeuristicResolverTest.cpp
@@ -41,7 +41,7 @@ template <typename InputNode, typename ParamT, typename InputMatcher,
           typename... OutputMatchers>
 void expectResolution(llvm::StringRef Code, ResolveFnT<ParamT> ResolveFn,
                       const InputMatcher &IM, const OutputMatchers &...OMS) {
-  auto TU = tooling::buildASTFromCodeWithArgs(Code, {"-std=c++20"});
+  auto TU = tooling::buildASTFromCodeWithArgs(Code, {"-std=c++23"});
   auto &Ctx = TU->getASTContext();
   auto InputMatches = match(IM, Ctx);
   ASSERT_EQ(1u, InputMatches.size());
@@ -449,6 +449,23 @@ TEST(HeuristicResolver, MemberExpr_DefaultTemplateArgument_Recursive) {
       cxxMethodDecl(hasName("foo")).bind("output"));
 }
 
+TEST(HeuristicResolver, MemberExpr_ExplicitObjectParameter) {
+  std::string Code = R"cpp(
+    struct Foo {
+      int m_int;
+
+      int bar(this auto&& self) {
+        return self.m_int;
+      }
+    };
+  )cpp";
+  // Test resolution of "m_int" in "self.m_int()".
+  expectResolution(
+      Code, &HeuristicResolver::resolveMemberExpr,
+      cxxDependentScopeMemberExpr(hasMemberName("m_int")).bind("input"),
+      fieldDecl(hasName("m_int")).bind("output"));
+}
+
 TEST(HeuristicResolver, DeclRefExpr_StaticMethod) {
   std::string Code = R"cpp(
     template <typename T>

@llvmbot
Copy link
Member

llvmbot commented Aug 24, 2025

@llvm/pr-subscribers-clang

Author: Mythreya Kuricheti (MythreyaK)

Changes

Fixes clangd/clangd#2323.

Assumes self is of the record type in the declaration.

struct Foo {
  int member {};
  auto&amp;&amp; getter1(this auto&amp;&amp; self) { // assume `self` is is `Foo`
    return self.member;
};

Full diff: https://github.com/llvm/llvm-project/pull/155143.diff

3 Files Affected:

  • (modified) clang-tools-extra/clangd/unittests/RenameTests.cpp (+42-1)
  • (modified) clang/lib/Sema/HeuristicResolver.cpp (+26)
  • (modified) clang/unittests/Sema/HeuristicResolverTest.cpp (+18-1)
diff --git a/clang-tools-extra/clangd/unittests/RenameTests.cpp b/clang-tools-extra/clangd/unittests/RenameTests.cpp
index 2cb0722f7f285..4701ae07d0d12 100644
--- a/clang-tools-extra/clangd/unittests/RenameTests.cpp
+++ b/clang-tools-extra/clangd/unittests/RenameTests.cpp
@@ -17,10 +17,11 @@
 #include "clang/Tooling/Core/Replacement.h"
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/Support/MemoryBuffer.h"
-#include <algorithm>
 #include "gmock/gmock.h"
 #include "gtest/gtest.h"
 
+#include <algorithm>
+
 namespace clang {
 namespace clangd {
 namespace {
@@ -2468,6 +2469,46 @@ TEST(CrossFileRenameTests, adjustmentCost) {
   }
 }
 
+TEST(RenameTest, RenameWithExplicitObjectPararameter) {
+  Annotations Test = {R"cpp(
+    struct Foo {
+      int [[memb^er]] {};
+      auto&& getter1(this auto&& self) {
+        auto local = [&] {
+          return self.[[memb^er]];
+        }();
+        return local + self.[[memb^er]];
+      }
+      auto&& getter2(this Foo&& self) {
+        return self.[[memb^er]];
+      }
+      int normal() {
+        return [[memb^er]];
+      }
+    };
+  )cpp"};
+
+  auto TU = TestTU::withCode(Test.code());
+  TU.ExtraArgs.push_back("-std=c++23");
+  auto AST = TU.build();
+
+  llvm::StringRef NewName = "m_member";
+  auto Index = TU.index();
+
+  for (const auto &RenamePos : Test.points()) {
+    auto RenameResult = rename({RenamePos, NewName, AST, testPath(TU.Filename),
+                                getVFSFromAST(AST), Index.get()});
+
+    ASSERT_TRUE(bool(RenameResult)) << RenameResult.takeError();
+    auto Res = RenameResult.get();
+
+    ASSERT_TRUE(bool(RenameResult)) << RenameResult.takeError();
+    ASSERT_EQ(1u, RenameResult->GlobalChanges.size());
+    EXPECT_EQ(applyEdits(std::move(RenameResult->GlobalChanges)).front().second,
+              expectedResult(Test, NewName));
+  }
+}
+
 } // namespace
 } // namespace clangd
 } // namespace clang
diff --git a/clang/lib/Sema/HeuristicResolver.cpp b/clang/lib/Sema/HeuristicResolver.cpp
index 933841beeac3d..20f7d0fca2066 100644
--- a/clang/lib/Sema/HeuristicResolver.cpp
+++ b/clang/lib/Sema/HeuristicResolver.cpp
@@ -14,6 +14,7 @@
 #include "clang/AST/TemplateBase.h"
 #include "clang/AST/Type.h"
 #include "llvm/ADT/identity.h"
+#include <optional>
 
 namespace clang {
 
@@ -301,9 +302,34 @@ std::vector<const NamedDecl *> HeuristicResolverImpl::resolveMemberExpr(
     return {};
   }
 
+  // check if member expr is in the context of an explicit object method
+  // If so, it's safe to assume the templated arg is of type of the record
+  const auto ExplicitMemberHeuristic =
+      [&](const Expr *Base) -> std::optional<QualType> {
+    if (auto *DeclRef = dyn_cast_if_present<DeclRefExpr>(Base)) {
+      auto *PrDecl = dyn_cast_if_present<ParmVarDecl>(DeclRef->getDecl());
+
+      if (PrDecl && PrDecl->isExplicitObjectParameter()) {
+        auto CxxRecord = dyn_cast_if_present<CXXRecordDecl>(
+            PrDecl->getDeclContext()->getParent());
+
+        if (CxxRecord) {
+          return Ctx.getTypeDeclType(dyn_cast<TypeDecl>(CxxRecord));
+        }
+      }
+    }
+
+    return std::nullopt;
+  };
+
   // Try resolving the member inside the expression's base type.
   Expr *Base = ME->isImplicitAccess() ? nullptr : ME->getBase();
   QualType BaseType = ME->getBaseType();
+
+  if (auto Type = ExplicitMemberHeuristic(Base)) {
+    BaseType = *Type;
+  }
+
   BaseType = simplifyType(BaseType, Base, ME->isArrow());
   return resolveDependentMember(BaseType, ME->getMember(), NoFilter);
 }
diff --git a/clang/unittests/Sema/HeuristicResolverTest.cpp b/clang/unittests/Sema/HeuristicResolverTest.cpp
index 7df25e01e66d4..21aca7a3489b8 100644
--- a/clang/unittests/Sema/HeuristicResolverTest.cpp
+++ b/clang/unittests/Sema/HeuristicResolverTest.cpp
@@ -41,7 +41,7 @@ template <typename InputNode, typename ParamT, typename InputMatcher,
           typename... OutputMatchers>
 void expectResolution(llvm::StringRef Code, ResolveFnT<ParamT> ResolveFn,
                       const InputMatcher &IM, const OutputMatchers &...OMS) {
-  auto TU = tooling::buildASTFromCodeWithArgs(Code, {"-std=c++20"});
+  auto TU = tooling::buildASTFromCodeWithArgs(Code, {"-std=c++23"});
   auto &Ctx = TU->getASTContext();
   auto InputMatches = match(IM, Ctx);
   ASSERT_EQ(1u, InputMatches.size());
@@ -449,6 +449,23 @@ TEST(HeuristicResolver, MemberExpr_DefaultTemplateArgument_Recursive) {
       cxxMethodDecl(hasName("foo")).bind("output"));
 }
 
+TEST(HeuristicResolver, MemberExpr_ExplicitObjectParameter) {
+  std::string Code = R"cpp(
+    struct Foo {
+      int m_int;
+
+      int bar(this auto&& self) {
+        return self.m_int;
+      }
+    };
+  )cpp";
+  // Test resolution of "m_int" in "self.m_int()".
+  expectResolution(
+      Code, &HeuristicResolver::resolveMemberExpr,
+      cxxDependentScopeMemberExpr(hasMemberName("m_int")).bind("input"),
+      fieldDecl(hasName("m_int")).bind("output"));
+}
+
 TEST(HeuristicResolver, DeclRefExpr_StaticMethod) {
   std::string Code = R"cpp(
     template <typename T>

Copy link

github-actions bot commented Aug 24, 2025

✅ With the latest revision this PR passed the C/C++ code formatter.

Comment on lines 310 to 311
auto *PrDecl = dyn_cast_if_present<ParmVarDecl>(DeclRef->getDecl());

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When is that null?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PrDecl is null here, for example.

DeclRefExpr 0x2e63d268 'auto' lvalue Var 0x2e63d150 'b' 'auto'

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the question is rather, when is DeclRef->getDecl() null, such that you need to use dyn_cast_if_present rather than plain dyn_cast.

Looking at other usages of DeclRefExpr::getDecl(), it seems callers assume it's never null.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated to use dyn_cast instead 👍

Comment on lines 305 to 306
// check if member expr is in the context of an explicit object method
// If so, it's safe to assume the templated arg is of type of the record
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider things like

struct S {
   template <typename T>
   void f(this Foo<T>);
};

It might be prudent to
1/ call simplifyType first
2/ only apply the heuristic to undeduced auto and TemplateTypeParmType

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to address this, but not sure if this is what you meant (I'm new to clang and still finding my way 😄). Please let me know what you think!

   template <typename T>
   void f(this Foo<T>);

Can a this parameter be something other than a record not related to S (Foo<T>, in this example)?

Copy link
Collaborator

@HighCommander4 HighCommander4 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! This looks generally good to me.

@@ -2468,6 +2469,46 @@ TEST(CrossFileRenameTests, adjustmentCost) {
}
}

TEST(RenameTest, RenameWithExplicitObjectPararameter) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of adding a new top-level test case (which involves repeating some boilerplate), could you add an entry to the array in RenameTest.WithinFileRename instead (and adjust the flags of that test to use -std=c++23 as necessary)?

// Try resolving the member inside the expression's base type.
Expr *Base = ME->isImplicitAccess() ? nullptr : ME->getBase();
QualType BaseType = ME->getBaseType();
BaseType = simplifyType(BaseType, Base, ME->isArrow());

if (!BaseType.isNull() &&
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we move this into simplifyType itself? Specifically, inside SimplifyOneStep, what are BaseType and Base here are available as T.Type and T.Expr respectively.

That would make this heuristic a bit more general. (For example, in combination with this patch which adds a simplifyType call site in SemaCodeComplete, it gets code completion to work after self.^ in such a function.)

@@ -256,6 +256,21 @@ QualType HeuristicResolverImpl::simplifyType(QualType Type, const Expr *E,
}
}
}
// check if member expr is in the context of an explicit object method
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would reword / expand on this as:

// Check if the expression refers to an explicit object parameter of templated type.
// If so, heuristically treat it as having the type of the enclosing class.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks!

(T.Type->isUndeducedAutoType() || T.Type->isTemplateTypeParmType())) {
if (auto *DRE = dyn_cast_if_present<DeclRefExpr>(T.E)) {
auto *PrDecl = dyn_cast_if_present<ParmVarDecl>(DRE->getDecl());
// Then the type of 'this' should be type of the record the method is
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(with the above change, this comment can be dropped)

BaseType = Type;
}
}
// fflush(stdout);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some debugging code snuck in here

Copy link
Contributor Author

@MythreyaK MythreyaK Aug 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh whoops, my bad 😅, removed, thanks!

@MythreyaK MythreyaK force-pushed the mythreyak/this-auto-heuristic branch from c76a451 to b48fa0e Compare August 27, 2025 02:19
Copy link
Collaborator

@HighCommander4 HighCommander4 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updates look good to me, just one outstanding issue.

@@ -41,7 +41,7 @@ template <typename InputNode, typename ParamT, typename InputMatcher,
typename... OutputMatchers>
void expectResolution(llvm::StringRef Code, ResolveFnT<ParamT> ResolveFn,
const InputMatcher &IM, const OutputMatchers &...OMS) {
auto TU = tooling::buildASTFromCodeWithArgs(Code, {"-std=c++23"});
auto TU = tooling::buildASTFromCodeWithArgs(Code, {"-std=c++20"});
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change (from c++20 to c++23) should be kept.

(It looks like the this auto test case manages to pass even without it, but that looks like a bug in the test suite; code with error diagnostics should cause the test to fail.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh whoops, done

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(It looks like the this auto test case manages to pass even without it, but that looks like a bug in the test suite; code with error diagnostics should cause the test to fail.)

(Filed #155545 for this.)

Copy link
Collaborator

@HighCommander4 HighCommander4 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

@HighCommander4 HighCommander4 merged commit f44eaf4 into llvm:main Aug 27, 2025
9 checks passed
@MythreyaK MythreyaK deleted the mythreyak/this-auto-heuristic branch August 27, 2025 06:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
clang:frontend Language frontend issues, e.g. anything involving "Sema" clang Clang issues not falling into any other category clang-tools-extra clangd
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Rename symbol doesnt rename symbols accessed via c++23 'deducing this' in a this auto method
6 participants