Coverage Report

Created: 2026-02-09 06:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/CMake/Source/cmForEachCommand.cxx
Line
Count
Source
1
/* Distributed under the OSI-approved BSD 3-Clause License.  See accompanying
2
   file LICENSE.rst or https://cmake.org/licensing for details.  */
3
#include "cmForEachCommand.h"
4
5
#include <algorithm>
6
#include <cassert>
7
#include <cstddef> // IWYU pragma: keep
8
// NOTE The declaration of `std::abs` has moved to `cmath` since C++17
9
// See https://en.cppreference.com/w/cpp/numeric/math/abs
10
// ALERT But IWYU used to lint `#include`s do not "understand"
11
// conditional compilation (i.e. `#if __cplusplus >= 201703L`)
12
#include <cstdlib>
13
#include <iterator>
14
#include <map>
15
#include <stdexcept>
16
#include <utility>
17
18
#include <cm/memory>
19
#include <cm/optional>
20
#include <cm/string_view>
21
#include <cmext/string_view>
22
23
#include "cmExecutionStatus.h"
24
#include "cmFunctionBlocker.h"
25
#include "cmList.h"
26
#include "cmListFileCache.h"
27
#include "cmMakefile.h"
28
#include "cmMessageType.h"
29
#include "cmPolicies.h"
30
#include "cmRange.h"
31
#include "cmStringAlgorithms.h"
32
#include "cmSystemTools.h"
33
#include "cmValue.h"
34
35
namespace {
36
class cmForEachFunctionBlocker : public cmFunctionBlocker
37
{
38
public:
39
  explicit cmForEachFunctionBlocker(cmMakefile* mf);
40
  ~cmForEachFunctionBlocker() override;
41
42
0
  cm::string_view StartCommandName() const override { return "foreach"_s; }
43
0
  cm::string_view EndCommandName() const override { return "endforeach"_s; }
44
45
  bool ArgumentsMatch(cmListFileFunction const& lff,
46
                      cmMakefile& mf) const override;
47
48
  bool Replay(std::vector<cmListFileFunction> functions,
49
              cmExecutionStatus& inStatus) override;
50
51
  void SetIterationVarsCount(std::size_t const varsCount)
52
0
  {
53
0
    this->IterationVarsCount = varsCount;
54
0
  }
55
0
  void SetZipLists() { this->ZipLists = true; }
56
57
  std::vector<std::string> Args;
58
59
private:
60
  struct InvokeResult
61
  {
62
    bool Restore;
63
    bool Break;
64
  };
65
66
  bool ReplayItems(std::vector<cmListFileFunction> const& functions,
67
                   cmExecutionStatus& inStatus);
68
69
  bool ReplayZipLists(std::vector<cmListFileFunction> const& functions,
70
                      cmExecutionStatus& inStatus);
71
72
  InvokeResult invoke(std::vector<cmListFileFunction> const& functions,
73
                      cmExecutionStatus& inStatus, cmMakefile& mf);
74
75
  cmMakefile* Makefile;
76
  std::size_t IterationVarsCount = 0u;
77
  bool ZipLists = false;
78
};
79
80
cmForEachFunctionBlocker::cmForEachFunctionBlocker(cmMakefile* mf)
81
0
  : Makefile(mf)
82
0
{
83
0
  this->Makefile->PushLoopBlock();
84
0
}
85
86
cmForEachFunctionBlocker::~cmForEachFunctionBlocker()
87
0
{
88
0
  this->Makefile->PopLoopBlock();
89
0
}
90
91
bool cmForEachFunctionBlocker::ArgumentsMatch(cmListFileFunction const& lff,
92
                                              cmMakefile& mf) const
93
0
{
94
0
  std::vector<std::string> expandedArguments;
95
0
  mf.ExpandArguments(lff.Arguments(), expandedArguments);
96
0
  return expandedArguments.empty() ||
97
0
    expandedArguments.front() == this->Args.front();
98
0
}
99
100
bool cmForEachFunctionBlocker::Replay(
101
  std::vector<cmListFileFunction> functions, cmExecutionStatus& inStatus)
102
0
{
103
0
  if (this->Args.size() == this->IterationVarsCount) {
104
0
    return true;
105
0
  }
106
0
  return this->ZipLists ? this->ReplayZipLists(functions, inStatus)
107
0
                        : this->ReplayItems(functions, inStatus);
108
0
}
109
110
bool cmForEachFunctionBlocker::ReplayItems(
111
  std::vector<cmListFileFunction> const& functions,
112
  cmExecutionStatus& inStatus)
113
0
{
114
0
  assert("Unexpected number of iteration variables" &&
115
0
         this->IterationVarsCount == 1);
116
117
0
  auto& mf = inStatus.GetMakefile();
118
119
  // At end of for each execute recorded commands
120
  // store the old value
121
0
  cm::optional<std::string> oldDef;
122
0
  if (mf.GetPolicyStatus(cmPolicies::CMP0124) != cmPolicies::NEW) {
123
0
    oldDef = mf.GetSafeDefinition(this->Args.front());
124
0
  } else if (mf.IsNormalDefinitionSet(this->Args.front())) {
125
0
    oldDef = *mf.GetDefinition(this->Args.front());
126
0
  }
127
128
0
  auto restore = false;
129
0
  for (std::string const& arg : cmMakeRange(this->Args).advance(1)) {
130
    // Set the variable to the loop value
131
0
    mf.AddDefinition(this->Args.front(), arg);
132
    // Invoke all the functions that were collected in the block.
133
0
    auto r = this->invoke(functions, inStatus, mf);
134
0
    restore = r.Restore;
135
0
    if (r.Break) {
136
0
      break;
137
0
    }
138
0
  }
139
140
0
  if (restore) {
141
0
    if (oldDef) {
142
      // restore the variable to its prior value
143
0
      mf.AddDefinition(this->Args.front(), *oldDef);
144
0
    } else {
145
0
      mf.RemoveDefinition(this->Args.front());
146
0
    }
147
0
  }
148
149
0
  return true;
150
0
}
151
152
bool cmForEachFunctionBlocker::ReplayZipLists(
153
  std::vector<cmListFileFunction> const& functions,
154
  cmExecutionStatus& inStatus)
155
0
{
156
0
  assert("Unexpected number of iteration variables" &&
157
0
         this->IterationVarsCount >= 1);
158
159
0
  auto& mf = inStatus.GetMakefile();
160
161
  // Expand the list of list-variables into a list of lists of strings
162
0
  std::vector<cmList> values;
163
0
  values.reserve(this->Args.size() - this->IterationVarsCount);
164
  // Also track the longest list size
165
0
  std::size_t maxItems = 0u;
166
0
  for (auto const& var :
167
0
       cmMakeRange(this->Args).advance(this->IterationVarsCount)) {
168
0
    cmList items;
169
0
    auto const& value = mf.GetSafeDefinition(var);
170
0
    if (!value.empty()) {
171
0
      items.assign(value, cmList::EmptyElements::Yes);
172
0
    }
173
0
    maxItems = std::max(maxItems, items.size());
174
0
    values.emplace_back(std::move(items));
175
0
  }
176
177
  // Form the list of iteration variables
178
0
  std::vector<std::string> iterationVars;
179
0
  if (this->IterationVarsCount > 1) {
180
    // If multiple iteration variables has given,
181
    // just copy them to the `iterationVars` list.
182
0
    iterationVars.reserve(values.size());
183
0
    std::copy(this->Args.begin(),
184
0
              this->Args.begin() + this->IterationVarsCount,
185
0
              std::back_inserter(iterationVars));
186
0
  } else {
187
    // In case of the only iteration variable,
188
    // generate names as `var_name_N`,
189
    // where `N` is the count of lists to zip
190
0
    iterationVars.resize(values.size());
191
0
    auto const iter_var_prefix = cmStrCat(this->Args.front(), '_');
192
0
    auto i = 0u;
193
0
    std::generate(
194
0
      iterationVars.begin(), iterationVars.end(),
195
0
      [&]() -> std::string { return cmStrCat(iter_var_prefix, i++); });
196
0
  }
197
0
  assert("Sanity check" && iterationVars.size() == values.size());
198
199
  // Store old values for iteration variables
200
0
  std::map<std::string, cm::optional<std::string>> oldDefs;
201
0
  for (auto i = 0u; i < values.size(); ++i) {
202
0
    auto const& varName = iterationVars[i];
203
0
    if (mf.GetPolicyStatus(cmPolicies::CMP0124) != cmPolicies::NEW) {
204
0
      oldDefs.emplace(varName, mf.GetSafeDefinition(varName));
205
0
    } else if (mf.IsNormalDefinitionSet(varName)) {
206
0
      oldDefs.emplace(varName, *mf.GetDefinition(varName));
207
0
    } else {
208
0
      oldDefs.emplace(varName, cm::nullopt);
209
0
    }
210
0
  }
211
212
  // Form a vector of current positions in all lists (Ok, vectors) of values
213
0
  std::vector<decltype(values)::value_type::iterator> positions;
214
0
  positions.reserve(values.size());
215
0
  std::transform(
216
0
    values.begin(), values.end(), std::back_inserter(positions),
217
    // Set the initial position to the beginning of every list
218
0
    [](decltype(values)::value_type& list) { return list.begin(); });
219
0
  assert("Sanity check" && positions.size() == values.size());
220
221
0
  auto restore = false;
222
  // Iterate over all the lists simulateneously
223
0
  for (auto i = 0u; i < maxItems; ++i) {
224
    // Declare iteration variables
225
0
    for (auto j = 0u; j < values.size(); ++j) {
226
      // Define (or not) the iteration variable if the current position
227
      // still not at the end...
228
0
      if (positions[j] != values[j].end()) {
229
0
        mf.AddDefinition(iterationVars[j], *positions[j]);
230
0
        ++positions[j];
231
0
      } else {
232
0
        mf.RemoveDefinition(iterationVars[j]);
233
0
      }
234
0
    }
235
    // Invoke all the functions that were collected in the block.
236
0
    auto r = this->invoke(functions, inStatus, mf);
237
0
    restore = r.Restore;
238
0
    if (r.Break) {
239
0
      break;
240
0
    }
241
0
  }
242
243
  // Restore the variables to its prior value
244
0
  if (restore) {
245
0
    for (auto const& p : oldDefs) {
246
0
      if (p.second) {
247
0
        mf.AddDefinition(p.first, *p.second);
248
0
      } else {
249
0
        mf.RemoveDefinition(p.first);
250
0
      }
251
0
    }
252
0
  }
253
0
  return true;
254
0
}
255
256
auto cmForEachFunctionBlocker::invoke(
257
  std::vector<cmListFileFunction> const& functions,
258
  cmExecutionStatus& inStatus, cmMakefile& mf) -> InvokeResult
259
0
{
260
0
  InvokeResult result = { true, false };
261
  // Invoke all the functions that were collected in the block.
262
0
  for (cmListFileFunction const& func : functions) {
263
0
    cmExecutionStatus status(mf);
264
0
    mf.ExecuteCommand(func, status);
265
0
    if (status.GetReturnInvoked()) {
266
0
      inStatus.SetReturnInvoked(status.GetReturnVariables());
267
0
      result.Break = true;
268
0
      break;
269
0
    }
270
0
    if (status.GetBreakInvoked()) {
271
0
      result.Break = true;
272
0
      break;
273
0
    }
274
0
    if (status.GetContinueInvoked()) {
275
0
      break;
276
0
    }
277
0
    if (status.HasExitCode()) {
278
0
      inStatus.SetExitCode(status.GetExitCode());
279
0
      result.Break = true;
280
0
      break;
281
0
    }
282
0
    if (cmSystemTools::GetFatalErrorOccurred()) {
283
0
      result.Restore = false;
284
0
      result.Break = true;
285
0
      break;
286
0
    }
287
0
  }
288
0
  return result;
289
0
}
290
291
bool HandleInMode(std::vector<std::string> const& args,
292
                  std::vector<std::string>::const_iterator kwInIter,
293
                  cmMakefile& makefile)
294
0
{
295
0
  assert("A valid iterator expected" && kwInIter != args.end());
296
297
0
  auto fb = cm::make_unique<cmForEachFunctionBlocker>(&makefile);
298
299
  // Copy iteration variable names first
300
0
  std::copy(args.begin(), kwInIter, std::back_inserter(fb->Args));
301
  // Remember the count of given iteration variable names
302
0
  auto const varsCount = fb->Args.size();
303
0
  fb->SetIterationVarsCount(varsCount);
304
305
0
  enum Doing
306
0
  {
307
0
    DoingNone,
308
0
    DoingLists,
309
0
    DoingItems,
310
0
    DoingZipLists
311
0
  };
312
0
  Doing doing = DoingNone;
313
  // Iterate over arguments past the "IN" keyword
314
0
  for (std::string const& arg : cmMakeRange(++kwInIter, args.end())) {
315
0
    if (arg == "LISTS") {
316
0
      if (doing == DoingZipLists) {
317
0
        makefile.IssueMessage(MessageType::FATAL_ERROR,
318
0
                              "ZIP_LISTS can not be used with LISTS or ITEMS");
319
0
        return true;
320
0
      }
321
0
      if (varsCount != 1u) {
322
0
        makefile.IssueMessage(
323
0
          MessageType::FATAL_ERROR,
324
0
          "ITEMS or LISTS require exactly one iteration variable");
325
0
        return true;
326
0
      }
327
0
      doing = DoingLists;
328
329
0
    } else if (arg == "ITEMS") {
330
0
      if (doing == DoingZipLists) {
331
0
        makefile.IssueMessage(MessageType::FATAL_ERROR,
332
0
                              "ZIP_LISTS can not be used with LISTS or ITEMS");
333
0
        return true;
334
0
      }
335
0
      if (varsCount != 1u) {
336
0
        makefile.IssueMessage(
337
0
          MessageType::FATAL_ERROR,
338
0
          "ITEMS or LISTS require exactly one iteration variable");
339
0
        return true;
340
0
      }
341
0
      doing = DoingItems;
342
343
0
    } else if (arg == "ZIP_LISTS") {
344
0
      if (doing != DoingNone) {
345
0
        makefile.IssueMessage(MessageType::FATAL_ERROR,
346
0
                              "ZIP_LISTS can not be used with LISTS or ITEMS");
347
0
        return true;
348
0
      }
349
0
      doing = DoingZipLists;
350
0
      fb->SetZipLists();
351
352
0
    } else if (doing == DoingLists) {
353
0
      auto const& value = makefile.GetSafeDefinition(arg);
354
0
      if (!value.empty()) {
355
0
        cmExpandList(value, fb->Args, cmList::EmptyElements::Yes);
356
0
      }
357
358
0
    } else if (doing == DoingItems || doing == DoingZipLists) {
359
0
      fb->Args.push_back(arg);
360
361
0
    } else {
362
0
      makefile.IssueMessage(MessageType::FATAL_ERROR,
363
0
                            cmStrCat("Unknown argument:\n  ", arg, '\n'));
364
0
      return true;
365
0
    }
366
0
  }
367
368
  // If `ZIP_LISTS` given and variables count more than 1,
369
  // make sure the given lists count matches variables...
370
0
  if (doing == DoingZipLists && varsCount > 1u &&
371
0
      (2u * varsCount) != fb->Args.size()) {
372
0
    makefile.IssueMessage(MessageType::FATAL_ERROR,
373
0
                          cmStrCat("Expected ", varsCount,
374
0
                                   " list variables, but given ",
375
0
                                   fb->Args.size() - varsCount));
376
0
    return true;
377
0
  }
378
379
0
  makefile.AddFunctionBlocker(std::move(fb));
380
381
0
  return true;
382
0
}
383
384
bool TryParseInteger(cmExecutionStatus& status, std::string const& str, int& i)
385
0
{
386
0
  try {
387
0
    i = std::stoi(str);
388
0
  } catch (std::invalid_argument const&) {
389
0
    status.SetError(cmStrCat("Invalid integer: '", str, '\''));
390
0
    cmSystemTools::SetFatalErrorOccurred();
391
0
    return false;
392
0
  } catch (std::out_of_range const&) {
393
0
    status.SetError(cmStrCat("Integer out of range: '", str, '\''));
394
0
    cmSystemTools::SetFatalErrorOccurred();
395
0
    return false;
396
0
  }
397
398
0
  return true;
399
0
}
400
401
} // anonymous namespace
402
403
bool cmForEachCommand(std::vector<std::string> const& args,
404
                      cmExecutionStatus& status)
405
0
{
406
0
  if (args.empty()) {
407
0
    status.SetError("called with incorrect number of arguments");
408
0
    return false;
409
0
  }
410
0
  auto kwInIter = std::find(args.begin(), args.end(), "IN");
411
0
  if (kwInIter != args.end()) {
412
0
    return HandleInMode(args, kwInIter, status.GetMakefile());
413
0
  }
414
415
  // create a function blocker
416
0
  auto fb = cm::make_unique<cmForEachFunctionBlocker>(&status.GetMakefile());
417
0
  if (args.size() > 1) {
418
0
    if (args[1] == "RANGE") {
419
0
      int start = 0;
420
0
      int stop = 0;
421
0
      int step = 0;
422
0
      if (args.size() == 3) {
423
0
        if (!TryParseInteger(status, args[2], stop)) {
424
0
          return false;
425
0
        }
426
0
      }
427
0
      if (args.size() == 4) {
428
0
        if (!TryParseInteger(status, args[2], start)) {
429
0
          return false;
430
0
        }
431
0
        if (!TryParseInteger(status, args[3], stop)) {
432
0
          return false;
433
0
        }
434
0
      }
435
0
      if (args.size() == 5) {
436
0
        if (!TryParseInteger(status, args[2], start)) {
437
0
          return false;
438
0
        }
439
0
        if (!TryParseInteger(status, args[3], stop)) {
440
0
          return false;
441
0
        }
442
0
        if (!TryParseInteger(status, args[4], step)) {
443
0
          return false;
444
0
        }
445
0
      }
446
0
      if (step == 0) {
447
0
        if (start > stop) {
448
0
          step = -1;
449
0
        } else {
450
0
          step = 1;
451
0
        }
452
0
      }
453
0
      if ((start > stop && step > 0) || (start < stop && step < 0) ||
454
0
          step == 0) {
455
0
        status.SetError(
456
0
          cmStrCat("called with incorrect range specification: start ", start,
457
0
                   ", stop ", stop, ", step ", step));
458
0
        cmSystemTools::SetFatalErrorOccurred();
459
0
        return false;
460
0
      }
461
462
      // Calculate expected iterations count and reserve enough space
463
      // in the `fb->Args` vector. The first item is the iteration variable
464
      // name...
465
0
      std::size_t const iter_cnt = 2u +
466
0
        static_cast<int>(start < stop) * (stop - start) / std::abs(step) +
467
0
        static_cast<int>(start > stop) * (start - stop) / std::abs(step);
468
0
      fb->Args.resize(iter_cnt);
469
0
      fb->Args.front() = args.front();
470
0
      auto cc = start;
471
0
      auto generator = [&cc, step]() -> std::string {
472
0
        auto result = std::to_string(cc);
473
0
        cc += step;
474
0
        return result;
475
0
      };
476
      // Fill the `range` vector w/ generated string values
477
      // (starting from 2nd position)
478
0
      std::generate(++fb->Args.begin(), fb->Args.end(), generator);
479
0
    } else {
480
0
      fb->Args = args;
481
0
    }
482
0
  } else {
483
0
    fb->Args = args;
484
0
  }
485
486
0
  fb->SetIterationVarsCount(1u);
487
0
  status.GetMakefile().AddFunctionBlocker(std::move(fb));
488
489
0
  return true;
490
0
}