GenEx: factor TRANSFORM selector parsing and selection

Give the AT/FOR/REGEX selector syntax shared by the $<LIST:TRANSFORM> actions a
single definition, so its diagnostics and index/REGEX semantics cannot drift
between actions.  Also let a caller learn which elements a selector picks
without running a transform, as groundwork for actions that drive their own
per-element loop.

Issue: #27892
This commit is contained in:
Mickaël Germain
2026-06-17 20:00:13 -07:00
parent 365c7d6af8
commit caa51f5689
3 changed files with 155 additions and 127 deletions

View File

@@ -1858,6 +1858,122 @@ inline cmList GetList(std::string const& list)
{
return list.empty() ? cmList{} : cmList{ list, cmList::EmptyElements::Yes };
}
// 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
// a malformed selector; empty `tokens` yields a select-all selector.
std::unique_ptr<cmList::TransformSelector> ParseTransformSelector(
std::vector<std::string> const& tokens, cm::GenEx::Evaluation* eval,
GeneratorExpressionContent const* content)
{
static std::string const REGEX{ "REGEX" };
static std::string const AT{ "AT" };
static std::string const FOR{ "FOR" };
std::unique_ptr<cmList::TransformSelector> selector;
std::size_t i = 0;
while (i < tokens.size()) {
std::string const& tok = tokens[i];
if ((tok == REGEX || tok == AT || tok == FOR) && selector) {
reportError(
eval, content->GetOriginalExpression(),
cmStrCat("sub-command TRANSFORM, selector already specified (",
selector->GetTag(), ")."));
return nullptr;
}
if (tok == REGEX) {
if (i + 1 >= tokens.size()) {
reportError(eval, content->GetOriginalExpression(),
"sub-command TRANSFORM, selector REGEX expects "
"'regular expression' argument.");
return nullptr;
}
selector =
cmList::TransformSelector::New<cmList::TransformSelector::REGEX>(
tokens[i + 1]);
i += 2;
continue;
}
if (tok == AT) {
++i;
std::vector<cmList::index_type> indexes;
for (; i < tokens.size(); ++i) {
cmList indexList{ tokens[i] };
for (auto const& index : indexList) {
cmList::index_type value;
if (!GetNumericArgument(index, value)) {
reportError(eval, content->GetOriginalExpression(),
cmStrCat("sub-command TRANSFORM, selector AT: '",
index, "': unexpected argument."));
return nullptr;
}
indexes.push_back(value);
}
}
if (indexes.empty()) {
reportError(eval, content->GetOriginalExpression(),
"sub-command TRANSFORM, selector AT expects at least one "
"numeric value.");
return nullptr;
}
selector = cmList::TransformSelector::New<cmList::TransformSelector::AT>(
std::move(indexes));
continue;
}
if (tok == FOR) {
if (i + 2 >= tokens.size()) {
reportError(eval, content->GetOriginalExpression(),
"sub-command TRANSFORM, selector FOR expects, at least, "
"two arguments.");
return nullptr;
}
cmList::index_type start = 0;
cmList::index_type stop = 0;
cmList::index_type step = 1;
if (!GetNumericArgument(tokens[i + 1], start) ||
!GetNumericArgument(tokens[i + 2], stop)) {
reportError(eval, content->GetOriginalExpression(),
"sub-command TRANSFORM, selector FOR expects, at least, "
"two numeric values.");
return nullptr;
}
i += 3;
if (i < tokens.size()) {
if (!GetNumericArgument(tokens[i], step)) {
step = -1;
}
++i;
}
if (step <= 0) {
reportError(eval, content->GetOriginalExpression(),
"sub-command TRANSFORM, selector FOR expects positive "
"numeric value for <step>.");
return nullptr;
}
selector =
cmList::TransformSelector::New<cmList::TransformSelector::FOR>(
{ start, stop, step });
continue;
}
std::vector<std::string> const rest(tokens.begin() + i, tokens.end());
reportError(eval, content->GetOriginalExpression(),
cmStrCat("sub-command TRANSFORM, '", cmJoin(rest, ", "),
"': unexpected argument(s)."));
return nullptr;
}
if (!selector) {
selector = cmList::TransformSelector::New();
}
return selector;
}
}
static const struct ListNode : public cmGeneratorExpressionNode
@@ -2177,137 +2293,14 @@ static const struct ListNode : public cmGeneratorExpressionNode
args.advance(descriptor->Arity);
}
std::string const REGEX{ "REGEX" };
std::string const AT{ "AT" };
std::string const FOR{ "FOR" };
std::unique_ptr<cmList::TransformSelector> selector;
try {
// handle optional arguments
while (!args.empty()) {
if ((args.front() == REGEX || args.front() == AT ||
args.front() == FOR) &&
selector) {
reportError(ev, cnt->GetOriginalExpression(),
cmStrCat("sub-command TRANSFORM, selector "
"already specified (",
selector->GetTag(), ")."));
return std::string{};
}
// REGEX selector
if (args.front() == REGEX) {
if (args.advance(1).empty()) {
reportError(
ev, cnt->GetOriginalExpression(),
"sub-command TRANSFORM, selector REGEX expects "
"'regular expression' argument.");
return std::string{};
}
selector = cmList::TransformSelector::New<
cmList::TransformSelector::REGEX>(args.front());
args.advance(1);
continue;
}
// AT selector
if (args.front() == AT) {
args.advance(1);
// get all specified indexes
std::vector<cmList::index_type> indexes;
while (!args.empty()) {
cmList indexList{ args.front() };
for (auto const& index : indexList) {
cmList::index_type value;
if (!GetNumericArgument(index, value)) {
// this is not a number, stop processing
reportError(
ev, cnt->GetOriginalExpression(),
cmStrCat("sub-command TRANSFORM, selector AT: '",
index, "': unexpected argument."));
return std::string{};
}
indexes.push_back(value);
}
args.advance(1);
}
if (indexes.empty()) {
reportError(ev, cnt->GetOriginalExpression(),
"sub-command TRANSFORM, selector AT "
"expects at least one "
"numeric value.");
return std::string{};
}
selector = cmList::TransformSelector::New<
cmList::TransformSelector::AT>(std::move(indexes));
continue;
}
// FOR selector
if (args.front() == FOR) {
if (args.advance(1).size() < 2) {
reportError(ev, cnt->GetOriginalExpression(),
"sub-command TRANSFORM, selector FOR "
"expects, at least,"
" two arguments.");
return std::string{};
}
cmList::index_type start = 0;
cmList::index_type stop = 0;
cmList::index_type step = 1;
bool valid = false;
if (GetNumericArgument(args.front(), start) &&
GetNumericArgument(args.advance(1).front(), stop)) {
valid = true;
}
if (!valid) {
reportError(
ev, cnt->GetOriginalExpression(),
"sub-command TRANSFORM, selector FOR expects, "
"at least, two numeric values.");
return std::string{};
}
// try to read a third numeric value for step
if (!args.advance(1).empty()) {
if (!GetNumericArgument(args.front(), step)) {
// this is not a number
step = -1;
}
args.advance(1);
}
if (step <= 0) {
reportError(
ev, cnt->GetOriginalExpression(),
"sub-command TRANSFORM, selector FOR expects "
"positive numeric value for <step>.");
return std::string{};
}
selector = cmList::TransformSelector::New<
cmList::TransformSelector::FOR>({ start, stop, step });
continue;
}
reportError(ev, cnt->GetOriginalExpression(),
cmStrCat("sub-command TRANSFORM, '",
cmJoin(args, ", "),
"': unexpected argument(s)."));
return std::string{};
}
std::vector<std::string> const tokens(args.begin(),
args.end());
selector = ParseTransformSelector(tokens, ev, cnt);
if (!selector) {
selector = cmList::TransformSelector::New();
return std::string{};
}
selector->Makefile = ev->Context.LG->GetMakefile();

View File

@@ -437,6 +437,17 @@ public:
std::transform(list.begin(), list.end(), list.begin(), transform);
}
// Return, for each element, whether the selector selects it via InSelection.
virtual std::vector<bool> Selection(cmList::container_type const& list)
{
std::vector<bool> selected;
selected.reserve(list.size());
for (auto const& value : list) {
selected.push_back(this->InSelection(value));
}
return selected;
}
protected:
TransformSelector(std::string&& tag)
: Tag(std::move(tag))
@@ -516,6 +527,19 @@ public:
}
}
// Select the computed Indexes; Validate throws transform_error on an
// out-of-range index.
std::vector<bool> Selection(cmList::container_type const& list) override
{
this->Validate(list.size());
std::vector<bool> selected(list.size(), false);
for (auto index : this->Indexes) {
selected[index] = true;
}
return selected;
}
protected:
TransformSelectorIndexes(std::string&& tag)
: TransformSelector(std::move(tag))
@@ -1148,6 +1172,12 @@ cmList& cmList::transform(TransformAction action, std::string const& arg,
return *this;
}
std::vector<bool> cmList::GetTransformSelection(
cmList::TransformSelector& selector) const
{
return static_cast<::TransformSelector&>(selector).Selection(this->Values);
}
std::string& cmList::append(std::string& list, std::string&& value)
{
if (list.empty()) {

View File

@@ -975,6 +975,11 @@ public:
cmMakefile& makefile,
std::unique_ptr<TransformSelector> = {});
// Return, for each element of this list, whether `selector` selects it.
// Throws transform_error on a malformed selector (e.g. an out-of-range
// index), like transform().
std::vector<bool> GetTransformSelection(TransformSelector& selector) const;
std::string join(cm::string_view glue) const
{
return cmList::Join(this->Values, glue);