mirror of
https://gitlab.kitware.com/cmake/cmake.git
synced 2026-09-25 04:09:36 +03:00
GenEx: add PREDICATE selector to $<LIST:TRANSFORM>
Add a PREDICATE selector to $<LIST:TRANSFORM> that chooses which elements to transform by evaluating a <body> per element (with $<_0> bound) and acting on those for which it yields "1". This selects by computed condition instead of the fixed AT/FOR/REGEX positions, so any generator expression -- including target queries -- can decide where an action applies. It works with both the canned actions and APPLY. Issue: #27892
This commit is contained in:
@@ -977,6 +977,22 @@ List Transformations
|
||||
|
||||
$<LIST:TRANSFORM,list,ACTION,REGEX,regular_expression>
|
||||
|
||||
``PREDICATE``
|
||||
Specify a generator expression ``body`` evaluated once per item with the
|
||||
bound operand :genex:`$<_0>` expanding to the current item. Only items
|
||||
whose body evaluates to ``1`` are transformed; the body must evaluate to
|
||||
exactly ``0`` or ``1``. ``PREDICATE`` may be combined with any action,
|
||||
including ``APPLY`` (in which case both bodies bind :genex:`$<_0>`
|
||||
independently).
|
||||
Like all selectors, only one selector may be given; ``PREDICATE`` cannot
|
||||
be combined with ``AT``, ``FOR``, or ``REGEX``.
|
||||
|
||||
.. code-block:: cmake
|
||||
|
||||
$<LIST:TRANSFORM,list,ACTION,PREDICATE,body>
|
||||
|
||||
.. versionadded:: 4.5
|
||||
|
||||
.. genex:: $<JOIN:list,glue>
|
||||
|
||||
Joins the ``list`` with the content of the ``glue`` string inserted between
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
genex-list-filter-transform-predicate
|
||||
-------------------------------------
|
||||
|
||||
* The :genex:`LIST` generator expression's ``TRANSFORM`` operation gained a
|
||||
``PREDICATE`` selector that chooses the items to transform by evaluating an
|
||||
arbitrary generator expression once per item, with ``$<_0>`` referring to the
|
||||
current item.
|
||||
@@ -149,6 +149,41 @@ static std::string EvaluateBodyWithBoundOperand(
|
||||
return result;
|
||||
}
|
||||
|
||||
// Evaluate `predicateBody` once per element of `list`, binding `$<_0>` to the
|
||||
// element (reusing EvaluateBodyWithBoundOperand). Each result must be exactly
|
||||
// "0" or "1". Returns the per-element boolean mask, or cm::nullopt after
|
||||
// reporting an error (non-boolean result, or a failure inside the body).
|
||||
static cm::optional<std::vector<bool>> EvaluatePredicateMask(
|
||||
cmGeneratorExpressionEvaluatorVector const& predicateBody,
|
||||
cmList const& list, cm::string_view subCommand, cm::GenEx::Evaluation* eval,
|
||||
GeneratorExpressionContent const* content,
|
||||
cmGeneratorExpressionDAGChecker* dagChecker)
|
||||
{
|
||||
std::vector<bool> mask;
|
||||
mask.reserve(list.size());
|
||||
for (auto const& element : list) {
|
||||
std::string r =
|
||||
EvaluateBodyWithBoundOperand(predicateBody, element, eval, dagChecker);
|
||||
if (eval->HadError) {
|
||||
return cm::nullopt;
|
||||
}
|
||||
if (r == "1") {
|
||||
mask.push_back(true);
|
||||
} else if (r == "0") {
|
||||
mask.push_back(false);
|
||||
} else {
|
||||
reportError(
|
||||
eval, content->GetOriginalExpression(),
|
||||
cmStrCat("sub-command ", subCommand,
|
||||
", PREDICATE body must evaluate to \"0\" or \"1\", but "
|
||||
"evaluated to \"",
|
||||
r, "\"."));
|
||||
return cm::nullopt;
|
||||
}
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
static const struct ZeroNode : public cmGeneratorExpressionNode
|
||||
{
|
||||
ZeroNode() {} // NOLINT(modernize-use-equals-default)
|
||||
@@ -1948,6 +1983,123 @@ cm::optional<TransformActionDescriptor> FindTransformActionDescriptor(
|
||||
return it->second;
|
||||
}
|
||||
|
||||
// Index in `parameters` at which a TRANSFORM action's selector region begins,
|
||||
// or nullopt if this is not a TRANSFORM or the action is unknown. For the
|
||||
// APPLY action the <body> occupies slot 3, so the selector starts at slot 4.
|
||||
cm::optional<std::size_t> TransformSelectorStart(
|
||||
std::vector<std::string> const& parameters)
|
||||
{
|
||||
if (parameters.size() < 3 || parameters[0] != "TRANSFORM") {
|
||||
return cm::nullopt;
|
||||
}
|
||||
std::string const& action = parameters[2];
|
||||
if (action == "APPLY") {
|
||||
return std::size_t{ 4 };
|
||||
}
|
||||
if (auto d = FindTransformActionDescriptor(action)) {
|
||||
return std::size_t{ 3 } + static_cast<std::size_t>(d->Arity);
|
||||
}
|
||||
return cm::nullopt;
|
||||
}
|
||||
|
||||
// Handle $<LIST:TRANSFORM,...,PREDICATE,<body>>. `predIndex` is the index of
|
||||
// the PREDICATE token in `parameters`; the <body> follows it. PREDICATE is
|
||||
// the sole selector: only elements whose predicate is "1" are transformed.
|
||||
std::string EvaluateTransformPredicate(
|
||||
std::vector<std::string> const& parameters, std::size_t predIndex,
|
||||
cm::GenEx::Evaluation* eval, GeneratorExpressionContent const* content,
|
||||
cmGeneratorExpressionDAGChecker* dagChecker)
|
||||
{
|
||||
// PREDICATE must take exactly one <body> and not be combined with another
|
||||
// selector (AT/FOR/REGEX) or trailing tokens.
|
||||
if (parameters.size() < predIndex + 2) {
|
||||
reportError(eval, content->GetOriginalExpression(),
|
||||
"sub-command TRANSFORM, selector PREDICATE expects a <body> "
|
||||
"argument.");
|
||||
return std::string();
|
||||
}
|
||||
if (parameters.size() > predIndex + 2) {
|
||||
reportError(eval, content->GetOriginalExpression(),
|
||||
"sub-command TRANSFORM, selector PREDICATE expects a single "
|
||||
"<body> argument and cannot be combined with another "
|
||||
"selector.");
|
||||
return std::string();
|
||||
}
|
||||
|
||||
cmList list = GetList(parameters[1]);
|
||||
if (list.empty()) {
|
||||
return std::string();
|
||||
}
|
||||
|
||||
cmGeneratorExpressionEvaluatorVector const& predicateBody =
|
||||
content->GetParamChildren()[predIndex + 1];
|
||||
auto mask = EvaluatePredicateMask(predicateBody, list, "TRANSFORM"_s, eval,
|
||||
content, dagChecker);
|
||||
if (!mask) {
|
||||
return std::string();
|
||||
}
|
||||
|
||||
if (parameters[2] == "APPLY") {
|
||||
cmGeneratorExpressionEvaluatorVector const& applyBody =
|
||||
content->GetParamChildren()[3];
|
||||
std::vector<std::string> out;
|
||||
out.reserve(list.size());
|
||||
std::size_t i = 0;
|
||||
for (auto const& element : list) {
|
||||
if ((*mask)[i]) {
|
||||
out.push_back(
|
||||
EvaluateBodyWithBoundOperand(applyBody, element, eval, dagChecker));
|
||||
if (eval->HadError) {
|
||||
return std::string();
|
||||
}
|
||||
} else {
|
||||
out.push_back(element);
|
||||
}
|
||||
++i;
|
||||
}
|
||||
return cmList{ out.begin(), out.end(), cmList::ExpandElements::No,
|
||||
cmList::EmptyElements::Yes }
|
||||
.to_string();
|
||||
}
|
||||
|
||||
std::string const& action = parameters[2];
|
||||
auto descriptor = FindTransformActionDescriptor(action);
|
||||
if (!descriptor) {
|
||||
reportError(
|
||||
eval, content->GetOriginalExpression(),
|
||||
cmStrCat(" sub-command TRANSFORM, ", action, " invalid action."));
|
||||
return std::string();
|
||||
}
|
||||
|
||||
// Action arguments occupy parameters[3 .. predIndex); TransformSelectorStart
|
||||
// guarantees there are exactly descriptor->Arity of them.
|
||||
std::vector<std::string> arguments(parameters.begin() + 3,
|
||||
parameters.begin() + predIndex);
|
||||
|
||||
std::vector<cmList::index_type> indices;
|
||||
for (std::size_t i = 0; i < mask->size(); ++i) {
|
||||
if ((*mask)[i]) {
|
||||
indices.push_back(static_cast<cmList::index_type>(i));
|
||||
}
|
||||
}
|
||||
if (indices.empty()) {
|
||||
// No element selected: TRANSFORM is a no-op.
|
||||
return list.to_string();
|
||||
}
|
||||
|
||||
auto selector =
|
||||
cmList::TransformSelector::New<cmList::TransformSelector::AT>(
|
||||
std::move(indices));
|
||||
selector->Makefile = eval->Context.LG->GetMakefile();
|
||||
try {
|
||||
return list.transform(descriptor->Action, arguments, std::move(selector))
|
||||
.to_string();
|
||||
} catch (cmList::transform_error& e) {
|
||||
reportError(eval, content->GetOriginalExpression(), e.what());
|
||||
return std::string();
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the optional trailing selector of a $<LIST:TRANSFORM,...> action
|
||||
// (AT <i>... / FOR <start> <stop> [<step>] / REGEX <re>) into a
|
||||
// cmList::TransformSelector. Returns nullptr (after reporting via `eval`) on
|
||||
@@ -2076,6 +2228,15 @@ static const struct ListNode : public cmGeneratorExpressionNode
|
||||
bool ShouldEvaluateNextParameter(std::vector<std::string> const& parameters,
|
||||
std::string&) const override
|
||||
{
|
||||
// Leave a TRANSFORM PREDICATE selector's <body> unevaluated. PREDICATE is
|
||||
// the selector keyword only when it sits exactly at the selector position
|
||||
// (not when it is a literal action argument such as APPEND PREDICATE).
|
||||
if (auto start = TransformSelectorStart(parameters)) {
|
||||
if (parameters.size() == *start + 1 &&
|
||||
parameters.back() == "PREDICATE") {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Skip the APPLY <body> (4th parameter) so $<_0> is not evaluated unbound;
|
||||
// selector args (5th+) evaluate normally.
|
||||
return !(parameters.size() == 3 && parameters[0] == "TRANSFORM" &&
|
||||
@@ -2087,6 +2248,18 @@ static const struct ListNode : public cmGeneratorExpressionNode
|
||||
GeneratorExpressionContent const* content,
|
||||
cmGeneratorExpressionDAGChecker* dagChecker) const override
|
||||
{
|
||||
// TRANSFORM ... PREDICATE <body>: genex-native predicate selector, usable
|
||||
// with any action (canned or APPLY). Handled here (not in the
|
||||
// listCommands lambda) because the predicate <body> needs the DAG checker.
|
||||
if (parameters.size() >= 3 && parameters[0] == "TRANSFORM") {
|
||||
if (auto start = TransformSelectorStart(parameters)) {
|
||||
if (*start < parameters.size() && parameters[*start] == "PREDICATE") {
|
||||
return EvaluateTransformPredicate(parameters, *start, eval, content,
|
||||
dagChecker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (parameters.size() >= 3 && parameters[0] == "TRANSFORM" &&
|
||||
parameters[2] == "APPLY") {
|
||||
if (parameters.size() < 4) {
|
||||
|
||||
@@ -16,6 +16,7 @@ set(CMakeLib_TESTS
|
||||
testGeneratedFileStream.cxx
|
||||
testGenExBoundOperand.cxx
|
||||
testGenExTransformApply.cxx
|
||||
testGenExListPredicate.cxx
|
||||
testJSONHelpers.cxx
|
||||
testRST.cxx
|
||||
testRange.cxx
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying
|
||||
file LICENSE.rst or https://cmake.org/licensing for details. */
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
|
||||
#include <cm/memory>
|
||||
|
||||
#include "cmGeneratorExpression.h"
|
||||
#include "cmGlobalGenerator.h"
|
||||
#include "cmLocalGenerator.h"
|
||||
#include "cmMakefile.h"
|
||||
#include "cmState.h"
|
||||
#include "cmStateDirectory.h"
|
||||
#include "cmStateSnapshot.h"
|
||||
#include "cmake.h"
|
||||
|
||||
namespace {
|
||||
struct GenExFixture
|
||||
{
|
||||
cmake CMake{ cmState::Role::Project };
|
||||
std::unique_ptr<cmGlobalGenerator> GG;
|
||||
std::unique_ptr<cmMakefile> MF;
|
||||
std::unique_ptr<cmLocalGenerator> LG;
|
||||
|
||||
GenExFixture()
|
||||
{
|
||||
this->GG = cm::make_unique<cmGlobalGenerator>(&this->CMake);
|
||||
cmStateSnapshot snapshot = this->CMake.GetCurrentSnapshot();
|
||||
snapshot.GetDirectory().SetCurrentBinary(".");
|
||||
snapshot.GetDirectory().SetCurrentSource(".");
|
||||
this->MF = cm::make_unique<cmMakefile>(this->GG.get(), snapshot);
|
||||
this->LG = this->GG->CreateLocalGenerator(this->MF.get());
|
||||
}
|
||||
|
||||
std::string Eval(std::string const& expr)
|
||||
{
|
||||
return cmGeneratorExpression::Evaluate(expr, this->LG.get(), "Debug");
|
||||
}
|
||||
};
|
||||
|
||||
bool expectEq(char const* name, std::string const& got,
|
||||
std::string const& want)
|
||||
{
|
||||
if (got != want) {
|
||||
std::cerr << name << ": expected '" << want << "', got '" << got << "'\n";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static bool testCannedTransformStillWorks()
|
||||
{
|
||||
GenExFixture fx;
|
||||
// Existing canned action + REGEX selector must be unaffected by the
|
||||
// refactor.
|
||||
return expectEq("testCannedTransformStillWorks",
|
||||
fx.Eval("$<LIST:TRANSFORM,foo;bar;baz,TOUPPER,REGEX,^ba>"),
|
||||
"foo;BAR;BAZ");
|
||||
}
|
||||
|
||||
static bool testTransformPredicateCanned()
|
||||
{
|
||||
GenExFixture fx;
|
||||
// PREPEND "X-" only to elements equal to "a"; others pass through.
|
||||
return expectEq(
|
||||
"testTransformPredicateCanned",
|
||||
fx.Eval(
|
||||
"$<LIST:TRANSFORM,a;b;a,PREPEND,X-,PREDICATE,$<STREQUAL:$<_0>,a>>"),
|
||||
"X-a;b;X-a");
|
||||
}
|
||||
|
||||
static bool testTransformPredicateNoneSelected()
|
||||
{
|
||||
GenExFixture fx;
|
||||
// No element matches: the list is returned unchanged.
|
||||
return expectEq("testTransformPredicateNoneSelected",
|
||||
fx.Eval("$<LIST:TRANSFORM,a;b;c,TOUPPER,PREDICATE,0>"),
|
||||
"a;b;c");
|
||||
}
|
||||
|
||||
static bool testTransformPredicateApply()
|
||||
{
|
||||
GenExFixture fx;
|
||||
// Upper-case only elements equal to "a"; "b" passes through unchanged.
|
||||
return expectEq("testTransformPredicateApply",
|
||||
fx.Eval("$<LIST:TRANSFORM,a;b;a,APPLY,$<UPPER_CASE:$<_0>>,"
|
||||
"PREDICATE,$<STREQUAL:$<_0>,a>>"),
|
||||
"A;b;A");
|
||||
}
|
||||
|
||||
static bool testTransformPredicateApplyShadowing()
|
||||
{
|
||||
GenExFixture fx;
|
||||
// The apply body and predicate body each independently bind $<_0>.
|
||||
return expectEq("testTransformPredicateApplyShadowing",
|
||||
fx.Eval("$<LIST:TRANSFORM,a;bb,APPLY,$<_0>$<_0>,PREDICATE,"
|
||||
"$<STREQUAL:$<_0>,a>>"),
|
||||
"aa;bb");
|
||||
}
|
||||
|
||||
static bool testTransformPredicateEmptyList()
|
||||
{
|
||||
GenExFixture fx;
|
||||
return expectEq("testTransformPredicateEmptyList",
|
||||
fx.Eval("$<LIST:TRANSFORM,,TOUPPER,PREDICATE,1>"), "");
|
||||
}
|
||||
|
||||
int testGenExListPredicate(int /*argc*/, char* /*argv*/[])
|
||||
{
|
||||
if (!testCannedTransformStillWorks()) {
|
||||
return 1;
|
||||
}
|
||||
if (!testTransformPredicateCanned()) {
|
||||
return 1;
|
||||
}
|
||||
if (!testTransformPredicateNoneSelected()) {
|
||||
return 1;
|
||||
}
|
||||
if (!testTransformPredicateApply()) {
|
||||
return 1;
|
||||
}
|
||||
if (!testTransformPredicateApplyShadowing()) {
|
||||
return 1;
|
||||
}
|
||||
if (!testTransformPredicateEmptyList()) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1 @@
|
||||
selector PREDICATE expects a single <body> argument
|
||||
@@ -0,0 +1,2 @@
|
||||
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/x.txt"
|
||||
CONTENT "$<LIST:TRANSFORM,a;b,TOUPPER,PREDICATE,1,AT,0>")
|
||||
@@ -0,0 +1,12 @@
|
||||
file(READ "${RunCMake_TEST_BINARY_DIR}/filtered.txt" actual)
|
||||
file(READ "${RunCMake_TEST_BINARY_DIR}/expected.txt" expected)
|
||||
string(STRIP "${actual}" actual)
|
||||
string(STRIP "${expected}" expected)
|
||||
# PREDICATE over app's LINK_LIBRARIES must prefix only STATIC_LIBRARY targets
|
||||
# and leave INTERFACE_LIBRARY targets unchanged.
|
||||
if(NOT actual STREQUAL expected)
|
||||
set(RunCMake_TEST_FAILED
|
||||
"PREDICATE-over-LINK_LIBRARIES output does not match the expected list:\n"
|
||||
" actual: [${actual}]\n"
|
||||
" expected: [${expected}]")
|
||||
endif()
|
||||
@@ -0,0 +1,28 @@
|
||||
enable_language(C)
|
||||
|
||||
# Evaluate LINK_LIBRARIES transitively (CMP0189, CMake 4.1+); the test dir's
|
||||
# cmake_minimum_required would otherwise leave this OLD.
|
||||
cmake_policy(SET CMP0189 NEW)
|
||||
|
||||
# A dependency tree with mixed library types:
|
||||
# app -> { netlib, plugin }; netlib -> { ssl, zlib }
|
||||
# netlib, ssl, zlib are STATIC_LIBRARY; plugin is INTERFACE_LIBRARY.
|
||||
add_library(ssl STATIC empty.c)
|
||||
add_library(zlib STATIC empty.c)
|
||||
add_library(plugin INTERFACE)
|
||||
add_library(netlib STATIC empty.c)
|
||||
target_link_libraries(netlib PUBLIC ssl zlib)
|
||||
add_library(app STATIC empty.c)
|
||||
target_link_libraries(app PRIVATE netlib plugin)
|
||||
|
||||
# PREDICATE body reads each element's TYPE via TARGET_PROPERTY, demonstrating
|
||||
# that the body is evaluated in a target context (context-sensitivity). Only
|
||||
# STATIC_LIBRARY targets receive the PREPEND; INTERFACE_LIBRARY targets are
|
||||
# left unchanged.
|
||||
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/filtered.txt"
|
||||
CONTENT "$<LIST:TRANSFORM,$<TARGET_PROPERTY:app,LINK_LIBRARIES>,PREPEND,lib:,PREDICATE,$<STREQUAL:$<TARGET_PROPERTY:$<_0>,TYPE>,STATIC_LIBRARY>>\n")
|
||||
|
||||
# Exact reference: direct deps first (netlib, plugin), then netlib's transitive
|
||||
# deps (ssl, zlib); only the three STATIC targets gain the "lib:" prefix.
|
||||
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/expected.txt"
|
||||
CONTENT "lib:netlib;plugin;lib:ssl;lib:zlib\n")
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1 @@
|
||||
selector PREDICATE expects a <body> argument
|
||||
@@ -0,0 +1,2 @@
|
||||
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/x.txt"
|
||||
CONTENT "$<LIST:TRANSFORM,a;b,TOUPPER,PREDICATE>")
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1 @@
|
||||
PREDICATE body must evaluate to "0" or "1"
|
||||
@@ -0,0 +1,2 @@
|
||||
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/x.txt"
|
||||
CONTENT "$<LIST:TRANSFORM,a;b,TOUPPER,PREDICATE,maybe>")
|
||||
@@ -64,6 +64,10 @@ run_cmake(ListTransformApplyNested)
|
||||
run_cmake(ListTransformApplyBadSelector)
|
||||
run_cmake(ListTransformApplyBodyError)
|
||||
run_cmake(ListTransformApplyMissingBody)
|
||||
run_cmake(ListTransformPredicateNonBool)
|
||||
run_cmake(ListTransformPredicateCombined)
|
||||
run_cmake(ListTransformPredicateMissingBody)
|
||||
run_cmake(ListTransformPredicateLinkLibraries)
|
||||
run_cmake(BoundOperandOutsideBinding)
|
||||
|
||||
function(run_cmake_build test)
|
||||
|
||||
Reference in New Issue
Block a user