Matching an AST node based on another matcher

For context, I’m writing a clang-tidy tool which should match the following (simplified criterias)

uint32_t x = foo();
uint32_t y = 42;

bar(x);
bar(y);

bar(x) should only be matched, because x is assigned from a function call. I understand I can do this with a two-step match, first looking for VarDecls that get assigned from a CallExpr, but I’m looking for something a little more idiomatic. However, my real-world use case makes it so that bar may take x as an argument, meaning this needs to get wrapped in optionally(...) (Which means I can’t use allOf(precondition, condition). So I set out to create a custom matcher which sort of looks like this:

	/// Matches nodes that match another query.
    /// 
    /// Given
    /// \code
    /// uint32_t y = 0;
    /// uint32_t x = foo();
    /// bar(x);
    /// bar(y);
    /// \endcode
    /// callExpr(hasArgument(0, declRefExpr(matchesMatcher(binaryOperator(hasOperands(declRefExpr(equalsBoundNode("declRef")), callExpr())), "declRef"))))
    ///   matches 'bar(x);' but not 'bar(y);'.
    AST_MATCHER_P2(clang::Stmt, matchesMatcher,
		clang::ast_matchers::internal::BindableMatcher<clang::Stmt>, InnerMatcher,
		llvm::StringRef, BoundName)
	{
		using namespace clang::ast_matchers;

		auto matchedNodesSet = match(
			allOf(
				expr(equalsNode(&Node)).bind(BoundName),
				InnerMatcher
			), Finder->getASTContext());

		auto itr = std::find_if(matchedNodesSet.begin(), matchedNodesSet.end(),
			[&](BoundNodes const& matchedNodes) {
				return matchedNodes.getNodeAs<clang::Stmt>(BoundName) != nullptr;
			}
		);
		return itr != matchedNodesSet.end();
	}

However, a couple questions:

  1. Is there a more idiomatic way to do this, that is already provided out of the box by AST matchers?
  2. Is this even a correct implementation?

I think you can do this with a single matcher if you do something along the lines of:

set traversal IgnoreUnlessSpelledInSource
m callExpr(hasAnyArgument(declRefExpr(to(varDecl(hasInitializer(callExpr()))))))

as in: Compiler Explorer

This does indeed work for this example, but what about more complex use cases where the varDecl would be a parameter to another callExpr?

Can you post some code examples so I can be sure we’re both thinking of the same scenarios?

Given this code, I want to match on “base::core::CAsset” through typeName:

createString(&typeName, "base::core::CAsset", 1);
initReflInfo(&meta::ReflInfo<base::core::CAsset>::g_instance, typeName);

My matcher would thus look like:

callExpr(
    hasArgument(1, declRefExpr(to(varDecl(
        matchesMatcher(callExpr(
            hasArgument(0, unaryOperator(hasOperatorName("&"),
                hasUnaryOperand(declRefExpr(to(varDecl(equalsBoundNode("this"))))),
            hasArgument(1, stringLiteral())
        ), "this")
    )
)

I’d need to somehow plucker the matches from matchesMatcher in the “parent” matcher, but exclude the pseudo-binding used to find the node of interest. However, I thought best to simplify the problem as much as possible, which meant ignoring the “binding to the string” part for the moment.

I’ve settled on this implementation, which appears to work. I’m not sure about the part where I inject matches; there’s probably a better way, but since I’m still not totally sure I understand how BoundNodesTreeBuilder works, I’ll keep it as is for now.

namespace internal {
    template <typename SourceType, typename TargetType>
    class matcher_MatchesMatcher final
        : public MatcherInterface<SourceType> {

    public:
        matcher_MatchesMatcher(DynTypedMatcher const& innerMatcher,
            llvm::StringRef const& boundName,
            bool injectMatches)
            : boundName_(boundName), injectMatches_(injectMatches),
            innerMatcher_(innerMatcher)
        { }

        bool matches(const SourceType& node, ASTMatchFinder* finder,
            BoundNodesTreeBuilder* builder) const override 
        {
            ASTContext& context = finder->getASTContext();
            TranslationUnitDecl* translationUnit = context.getTranslationUnitDecl();
            DynTypedNode dynNode = DynTypedNode::create(node);

            BoundNodesTreeBuilder subTree(*builder);
            subTree.setBinding(boundName_, dynNode);

            bool matchesAnyDescendant = finder->matchesDescendantOf(*translationUnit,
                innerMatcher_,
                &subTree,
                ASTMatchFinder::BK_All);

            if (injectMatches_) {
                struct Visitor final : BoundNodesTreeBuilder::Visitor {
                    Visitor(BoundNodesTreeBuilder& subTree, llvm::StringRef const& boundName) : _subTree(subTree), _boundName(boundName) { }

                    void visitMatch(const BoundNodes& BoundNodesView) override {
                        for (auto&& [k, v] : BoundNodesView.getMap())
                            if (k != _boundName)
                                _subTree.setBinding(k, v);
                    }

                private:
                    BoundNodesTreeBuilder& _subTree;
                    llvm::StringRef const& _boundName;
                } visitor(*builder, boundName_);

                subTree.visitMatches(&visitor);
            }

            return matchesAnyDescendant;
        }

    private:
        DynTypedMatcher innerMatcher_;
        llvm::StringRef boundName_;
        bool injectMatches_;
    };
}

/// Matches nodes that match another query.
/// 
/// Given
/// \code
/// uint32_t y = 0;
/// uint32_t x = foo();
/// bar(x);
/// bar(y);
/// \endcode
/// 
/// callExpr(hasArgument(0, declRefExpr(matchesMatcher(binaryOperator(hasOperands(declRefExpr(equalsBoundNode("declRef")), callExpr())), "declRef"))))
///   matches 'bar(x);' but not 'bar(y);'.
template <typename SourceType, typename TargetType>
auto matchesMatcher(Matcher<TargetType> const& matcher,
    llvm::StringRef const& boundName, bool keepMatches) {
    return Matcher<SourceType>(
        new internal::matcher_MatchesMatcher<SourceType, TargetType>(matcher, boundName, keepMatches)
    );
}

Example of one of my use cases:

declRefExpr(to(
    varDecl(
        custom_matchers::matchesMatcher<VarDecl>(
            binaryOperator(hasOperatorName("="), hasOperands(
                declRefExpr(to(equalsBoundNode("this"))),
                ignoringParenCasts(
                    callExpr(argumentCountIs(0), callee(
                        functionDecl(hasAttr(attr::Annotate)).bind(Parameters::BaseType))
                    )
                )
            )), "this", true
            )
    )
))

Not perfect (I’d like to get rid of the template parameter), but I don’t think I can do better for now.