mirror of
https://gitlab.kitware.com/cmake/cmake.git
synced 2026-09-27 04:09:36 +03:00
Add an APPLY action to $<LIST:TRANSFORM> that evaluates an arbitrary <body> once per selected element, with $<_0> bound to the element, so a list can be mapped through any generator expression at generate time. Unlike the configure-time list(TRANSFORM ... APPLY <function>) command, the genex form has no side effects and returns the body's value directly, and a list-valued result expands into multiple elements. The body evaluates in its own binding scope, so nested APPLY actions can shadow $<_0>, and context-sensitive state it observes (such as target dependencies) still propagates to the enclosing expression. APPLY accepts the same AT/FOR/REGEX selectors as the canned actions. Issue: #27892
175 lines
4.4 KiB
C++
175 lines
4.4 KiB
C++
/* 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");
|
|
}
|
|
};
|
|
}
|
|
|
|
static bool testApplyBasic()
|
|
{
|
|
GenExFixture fx;
|
|
return fx.Eval("$<LIST:TRANSFORM,net;audio,APPLY,$<UPPER_CASE:$<_0>>>") ==
|
|
"NET;AUDIO";
|
|
}
|
|
|
|
static bool testApplyConstantBody()
|
|
{
|
|
// The body need not reference the operand at all.
|
|
GenExFixture fx;
|
|
return fx.Eval("$<LIST:TRANSFORM,a;b;c,APPLY,X>") == "X;X;X";
|
|
}
|
|
|
|
static bool testApplyOperandUsedTwice()
|
|
{
|
|
// The operand may be substituted more than once in a single body.
|
|
GenExFixture fx;
|
|
return fx.Eval("$<LIST:TRANSFORM,a;b,APPLY,$<_0>$<_0>>") == "aa;bb";
|
|
}
|
|
|
|
static bool testApplyFlatMapExpands()
|
|
{
|
|
GenExFixture fx;
|
|
// A list-valued body expands: a -> "x;y", b -> "z" => 3 items.
|
|
std::string out =
|
|
fx.Eval("$<LIST:TRANSFORM,a;b,APPLY,$<IF:$<STREQUAL:$<_0>,a>,x;y,z>>");
|
|
std::string len = fx.Eval("$<LIST:LENGTH,$<LIST:TRANSFORM,a;b,APPLY,"
|
|
"$<IF:$<STREQUAL:$<_0>,a>,x;y,z>>>");
|
|
return out == "x;y;z" && len == "3";
|
|
}
|
|
|
|
static bool testApplySelector()
|
|
{
|
|
GenExFixture fx;
|
|
return fx.Eval(
|
|
"$<LIST:TRANSFORM,a;b;c;d,APPLY,$<UPPER_CASE:$<_0>>,AT,0,2>") ==
|
|
"A;b;C;d";
|
|
}
|
|
|
|
static bool testApplyNestedShadowing()
|
|
{
|
|
GenExFixture fx;
|
|
return fx.Eval(
|
|
"$<LIST:TRANSFORM,a;b,APPLY,"
|
|
"$<LIST:TRANSFORM,$<_0>1;$<_0>2,APPLY,$<UPPER_CASE:$<_0>>>>") ==
|
|
"A1;A2;B1;B2";
|
|
}
|
|
|
|
static bool testApplyEmptyList()
|
|
{
|
|
GenExFixture fx;
|
|
// An empty list produces an empty string without error (matches canned
|
|
// TRANSFORM behavior).
|
|
std::string got = fx.Eval("$<LIST:TRANSFORM,,APPLY,x$<_0>y>");
|
|
if (!got.empty()) {
|
|
std::cerr << "testApplyEmptyList: expected empty string, got '" << got
|
|
<< "'\n";
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool testApplyEmptyElement()
|
|
{
|
|
GenExFixture fx;
|
|
// Every element is selected, including the leading empty one, so the body
|
|
// wraps each: "" -> "xy", "b" -> "xby".
|
|
return fx.Eval("$<LIST:TRANSFORM,;b,APPLY,x$<_0>y>") == "xy;xby";
|
|
}
|
|
|
|
static bool testApplyEmptyPassthrough()
|
|
{
|
|
GenExFixture fx;
|
|
// The list ";b" has two elements: "" (index 0, unselected) and "b"
|
|
// (index 1, selected). The unselected empty element must pass through
|
|
// verbatim, not be silently dropped.
|
|
std::string got = fx.Eval("$<LIST:TRANSFORM,;b,APPLY,X$<_0>Y,AT,1>");
|
|
if (got != ";XbY") {
|
|
std::cerr << "testApplyEmptyPassthrough: expected ';XbY', got '" << got
|
|
<< "'\n";
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
static bool testApplyEmptyResult()
|
|
{
|
|
GenExFixture fx;
|
|
// Body yields "" for "a" and "z" for "b". The empty element produced by
|
|
// the body must not be dropped.
|
|
std::string got =
|
|
fx.Eval("$<LIST:TRANSFORM,a;b,APPLY,$<IF:$<STREQUAL:$<_0>,a>,,z>>");
|
|
if (got != ";z") {
|
|
std::cerr << "testApplyEmptyResult: expected ';z', got '" << got << "'\n";
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
int testGenExTransformApply(int /*argc*/, char* /*argv*/[])
|
|
{
|
|
if (!testApplyBasic()) {
|
|
return 1;
|
|
}
|
|
if (!testApplyConstantBody()) {
|
|
return 1;
|
|
}
|
|
if (!testApplyOperandUsedTwice()) {
|
|
return 1;
|
|
}
|
|
if (!testApplyFlatMapExpands()) {
|
|
return 1;
|
|
}
|
|
if (!testApplySelector()) {
|
|
return 1;
|
|
}
|
|
if (!testApplyNestedShadowing()) {
|
|
return 1;
|
|
}
|
|
if (!testApplyEmptyList()) {
|
|
return 1;
|
|
}
|
|
if (!testApplyEmptyElement()) {
|
|
return 1;
|
|
}
|
|
if (!testApplyEmptyPassthrough()) {
|
|
return 1;
|
|
}
|
|
if (!testApplyEmptyResult()) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}
|