Coverage Report

Created: 2026-02-09 06:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/CMake/Source/cmListFileCache.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
#define cmListFileCache_cxx
4
#include "cmListFileCache.h"
5
6
#include <memory>
7
#include <ostream>
8
#include <utility>
9
10
#ifdef _WIN32
11
#  include <cmsys/Encoding.hxx>
12
#endif
13
14
#include <cm/string_view>
15
16
#include "cmList.h"
17
#include "cmListFileLexer.h"
18
#include "cmMessageType.h"
19
#include "cmMessenger.h"
20
#include "cmStringAlgorithms.h"
21
#include "cmSystemTools.h"
22
23
namespace {
24
25
enum class NestingStateEnum
26
{
27
  If,
28
  Else,
29
  While,
30
  Foreach,
31
  Function,
32
  Macro,
33
  Block
34
};
35
36
struct NestingState
37
{
38
  NestingStateEnum State;
39
  cmListFileContext Context;
40
};
41
42
bool TopIs(std::vector<NestingState>& stack, NestingStateEnum state)
43
0
{
44
0
  return !stack.empty() && stack.back().State == state;
45
0
}
46
47
class cmListFileParser
48
{
49
public:
50
  cmListFileParser(cmListFile* lf, cmListFileBacktrace lfbt,
51
                   cmMessenger* messenger);
52
  cmListFileParser(cmListFileParser const&) = delete;
53
  cmListFileParser& operator=(cmListFileParser const&) = delete;
54
55
  bool ParseFile(char const* filename);
56
  bool ParseString(cm::string_view str, char const* virtual_filename);
57
58
private:
59
  bool Parse();
60
  bool ParseFunction(cm::string_view name, long line);
61
  bool AddArgument(cmListFileLexer_Token* token,
62
                   cmListFileArgument::Delimiter delim);
63
  void IssueFileOpenError(std::string const& text) const;
64
  void IssueError(std::string const& text) const;
65
66
  cm::optional<cmListFileContext> CheckNesting() const;
67
68
  enum
69
  {
70
    SeparationOkay,
71
    SeparationWarning,
72
    SeparationError
73
  } Separation;
74
75
  cmListFile* ListFile;
76
  cmListFileBacktrace Backtrace;
77
  cmMessenger* Messenger;
78
  char const* FileName = nullptr;
79
  std::unique_ptr<cmListFileLexer, void (*)(cmListFileLexer*)> Lexer;
80
  std::string FunctionName;
81
  long FunctionLine;
82
  long FunctionLineEnd;
83
  std::vector<cmListFileArgument> FunctionArguments;
84
};
85
86
cmListFileParser::cmListFileParser(cmListFile* lf, cmListFileBacktrace lfbt,
87
                                   cmMessenger* messenger)
88
1
  : ListFile(lf)
89
1
  , Backtrace(std::move(lfbt))
90
1
  , Messenger(messenger)
91
1
  , Lexer(cmListFileLexer_New(), cmListFileLexer_Delete)
92
1
{
93
1
}
94
95
void cmListFileParser::IssueFileOpenError(std::string const& text) const
96
0
{
97
0
  this->Messenger->IssueMessage(MessageType::FATAL_ERROR, text,
98
0
                                this->Backtrace);
99
0
}
100
101
void cmListFileParser::IssueError(std::string const& text) const
102
1
{
103
1
  cmListFileContext lfc;
104
1
  lfc.FilePath = this->FileName;
105
1
  lfc.Line = cmListFileLexer_GetCurrentLine(this->Lexer.get());
106
1
  cmListFileBacktrace lfbt = this->Backtrace;
107
1
  lfbt = lfbt.Push(lfc);
108
1
  this->Messenger->IssueMessage(MessageType::FATAL_ERROR, text, lfbt);
109
1
  cmSystemTools::SetFatalErrorOccurred();
110
1
}
111
112
bool cmListFileParser::ParseFile(char const* filename)
113
1
{
114
1
  this->FileName = filename;
115
116
#ifdef _WIN32
117
  std::string expandedFileName = cmsys::Encoding::ToNarrow(
118
    cmSystemTools::ConvertToWindowsExtendedPath(filename));
119
  filename = expandedFileName.c_str();
120
#endif
121
122
  // Open the file.
123
1
  cmListFileLexer_BOM bom;
124
1
  if (!cmListFileLexer_SetFileName(this->Lexer.get(), filename, &bom)) {
125
0
    this->IssueFileOpenError("cmListFileCache: error can not open file.");
126
0
    return false;
127
0
  }
128
129
  // Verify the Byte-Order-Mark, if any.
130
1
  if (bom != cmListFileLexer_BOM_None && bom != cmListFileLexer_BOM_UTF8) {
131
0
    cmListFileLexer_SetFileName(this->Lexer.get(), nullptr, nullptr);
132
0
    this->IssueFileOpenError(
133
0
      "File starts with a Byte-Order-Mark that is not UTF-8.");
134
0
    return false;
135
0
  }
136
137
1
  return this->Parse();
138
1
}
139
140
bool cmListFileParser::ParseString(cm::string_view str,
141
                                   char const* virtual_filename)
142
0
{
143
0
  this->FileName = virtual_filename;
144
145
0
  if (!cmListFileLexer_SetString(this->Lexer.get(), str.data(),
146
0
                                 str.length())) {
147
0
    this->IssueFileOpenError("cmListFileCache: cannot allocate buffer.");
148
0
    return false;
149
0
  }
150
151
0
  return this->Parse();
152
0
}
153
154
bool cmListFileParser::Parse()
155
1
{
156
  // Use a simple recursive-descent parser to process the token
157
  // stream.
158
1
  bool haveNewline = true;
159
1
  while (cmListFileLexer_Token* token =
160
1
           cmListFileLexer_Scan(this->Lexer.get())) {
161
1
    if (token->type == cmListFileLexer_Token_Space) {
162
1
    } else if (token->type == cmListFileLexer_Token_Newline) {
163
0
      haveNewline = true;
164
1
    } else if (token->type == cmListFileLexer_Token_CommentBracket) {
165
0
      haveNewline = false;
166
1
    } else if (token->type == cmListFileLexer_Token_Identifier) {
167
0
      if (haveNewline) {
168
0
        haveNewline = false;
169
0
        if (this->ParseFunction(cm::string_view(token->text, token->length),
170
0
                                token->line)) {
171
0
          this->ListFile->Functions.emplace_back(
172
0
            std::move(this->FunctionName), this->FunctionLine,
173
0
            this->FunctionLineEnd, std::move(this->FunctionArguments));
174
0
        } else {
175
0
          return false;
176
0
        }
177
0
      } else {
178
0
        auto error = cmStrCat(
179
0
          "Parse error.  Expected a newline, got ",
180
0
          cmListFileLexer_GetTypeAsString(this->Lexer.get(), token->type),
181
0
          " with text \"", cm::string_view(token->text, token->length), "\".");
182
0
        this->IssueError(error);
183
0
        return false;
184
0
      }
185
1
    } else {
186
1
      auto error = cmStrCat(
187
1
        "Parse error.  Expected a command name, got ",
188
1
        cmListFileLexer_GetTypeAsString(this->Lexer.get(), token->type),
189
1
        " with text \"", cm::string_view(token->text, token->length), "\".");
190
1
      this->IssueError(error);
191
1
      return false;
192
1
    }
193
1
  }
194
195
  // Check if all functions are nested properly.
196
0
  if (auto badNesting = this->CheckNesting()) {
197
0
    this->Messenger->IssueMessage(
198
0
      MessageType::FATAL_ERROR,
199
0
      "Flow control statements are not properly nested.",
200
0
      this->Backtrace.Push(*badNesting));
201
0
    cmSystemTools::SetFatalErrorOccurred();
202
0
    return false;
203
0
  }
204
205
0
  return true;
206
0
}
207
208
bool cmListFileParser::ParseFunction(cm::string_view name, long line)
209
0
{
210
  // Ininitialize a new function call.
211
0
  this->FunctionName.assign(name.data(), name.size());
212
0
  this->FunctionLine = line;
213
214
  // Command name has already been parsed.  Read the left paren.
215
0
  cmListFileLexer_Token* token;
216
0
  while ((token = cmListFileLexer_Scan(this->Lexer.get())) &&
217
0
         token->type == cmListFileLexer_Token_Space) {
218
0
  }
219
0
  if (!token) {
220
0
    this->IssueError("Unexpected end of file.\n"
221
0
                     "Parse error.  Function missing opening \"(\".");
222
0
    return false;
223
0
  }
224
0
  if (token->type != cmListFileLexer_Token_ParenLeft) {
225
0
    auto error = cmStrCat(
226
0
      "Parse error.  Expected \"(\", got ",
227
0
      cmListFileLexer_GetTypeAsString(this->Lexer.get(), token->type),
228
0
      " with text \"", cm::string_view(token->text, token->length), "\".");
229
0
    this->IssueError(error);
230
0
    return false;
231
0
  }
232
233
  // Arguments.
234
0
  unsigned long parenDepth = 0;
235
0
  this->Separation = SeparationOkay;
236
0
  while ((token = cmListFileLexer_Scan(this->Lexer.get()))) {
237
0
    if (token->type == cmListFileLexer_Token_Space ||
238
0
        token->type == cmListFileLexer_Token_Newline) {
239
0
      this->Separation = SeparationOkay;
240
0
      continue;
241
0
    }
242
0
    if (token->type == cmListFileLexer_Token_ParenLeft) {
243
0
      parenDepth++;
244
0
      this->Separation = SeparationOkay;
245
0
      if (!this->AddArgument(token, cmListFileArgument::Unquoted)) {
246
0
        return false;
247
0
      }
248
0
    } else if (token->type == cmListFileLexer_Token_ParenRight) {
249
0
      if (parenDepth == 0) {
250
0
        this->FunctionLineEnd = token->line;
251
0
        return true;
252
0
      }
253
0
      parenDepth--;
254
0
      this->Separation = SeparationOkay;
255
0
      if (!this->AddArgument(token, cmListFileArgument::Unquoted)) {
256
0
        return false;
257
0
      }
258
0
      this->Separation = SeparationWarning;
259
0
    } else if (token->type == cmListFileLexer_Token_Identifier ||
260
0
               token->type == cmListFileLexer_Token_ArgumentUnquoted) {
261
0
      if (!this->AddArgument(token, cmListFileArgument::Unquoted)) {
262
0
        return false;
263
0
      }
264
0
      this->Separation = SeparationWarning;
265
0
    } else if (token->type == cmListFileLexer_Token_ArgumentQuoted) {
266
0
      if (!this->AddArgument(token, cmListFileArgument::Quoted)) {
267
0
        return false;
268
0
      }
269
0
      this->Separation = SeparationWarning;
270
0
    } else if (token->type == cmListFileLexer_Token_ArgumentBracket) {
271
0
      if (!this->AddArgument(token, cmListFileArgument::Bracket)) {
272
0
        return false;
273
0
      }
274
0
      this->Separation = SeparationError;
275
0
    } else if (token->type == cmListFileLexer_Token_CommentBracket) {
276
0
      this->Separation = SeparationError;
277
0
    } else {
278
      // Error.
279
0
      auto error = cmStrCat(
280
0
        "Parse error.  Function missing ending \")\".  "
281
0
        "Instead found ",
282
0
        cmListFileLexer_GetTypeAsString(this->Lexer.get(), token->type),
283
0
        " with text \"", cm::string_view(token->text, token->length), "\".");
284
0
      this->IssueError(error);
285
0
      return false;
286
0
    }
287
0
  }
288
289
0
  cmListFileContext lfc;
290
0
  lfc.FilePath = this->FileName;
291
0
  lfc.Line = line;
292
0
  cmListFileBacktrace lfbt = this->Backtrace;
293
0
  lfbt = lfbt.Push(lfc);
294
0
  this->Messenger->IssueMessage(
295
0
    MessageType::FATAL_ERROR,
296
0
    "Parse error.  Function missing ending \")\".  "
297
0
    "End of file reached.",
298
0
    lfbt);
299
0
  return false;
300
0
}
301
302
bool cmListFileParser::AddArgument(cmListFileLexer_Token* token,
303
                                   cmListFileArgument::Delimiter delim)
304
0
{
305
0
  this->FunctionArguments.emplace_back(
306
0
    cm::string_view(token->text, token->length), delim, token->line);
307
0
  if (this->Separation == SeparationOkay) {
308
0
    return true;
309
0
  }
310
0
  bool isError = (this->Separation == SeparationError ||
311
0
                  delim == cmListFileArgument::Bracket);
312
0
  cmListFileContext lfc;
313
0
  lfc.FilePath = this->FileName;
314
0
  lfc.Line = token->line;
315
0
  cmListFileBacktrace lfbt = this->Backtrace;
316
0
  lfbt = lfbt.Push(lfc);
317
0
  auto msg =
318
0
    cmStrCat("Syntax ", (isError ? "Error" : "Warning"),
319
0
             " in cmake code at column ", token->column,
320
0
             "\n"
321
0
             "Argument not separated from preceding token by whitespace.");
322
0
  if (isError) {
323
0
    this->Messenger->IssueMessage(MessageType::FATAL_ERROR, msg, lfbt);
324
0
    return false;
325
0
  }
326
0
  this->Messenger->IssueMessage(MessageType::AUTHOR_WARNING, msg, lfbt);
327
0
  return true;
328
0
}
329
330
cm::optional<cmListFileContext> cmListFileParser::CheckNesting() const
331
0
{
332
0
  std::vector<NestingState> stack;
333
334
0
  for (auto const& func : this->ListFile->Functions) {
335
0
    auto const& name = func.LowerCaseName();
336
0
    if (name == "if") {
337
0
      stack.push_back({
338
0
        NestingStateEnum::If,
339
0
        cmListFileContext::FromListFileFunction(func, this->FileName),
340
0
      });
341
0
    } else if (name == "elseif") {
342
0
      if (!TopIs(stack, NestingStateEnum::If)) {
343
0
        return cmListFileContext::FromListFileFunction(func, this->FileName);
344
0
      }
345
0
      stack.back() = {
346
0
        NestingStateEnum::If,
347
0
        cmListFileContext::FromListFileFunction(func, this->FileName),
348
0
      };
349
0
    } else if (name == "else") {
350
0
      if (!TopIs(stack, NestingStateEnum::If)) {
351
0
        return cmListFileContext::FromListFileFunction(func, this->FileName);
352
0
      }
353
0
      stack.back() = {
354
0
        NestingStateEnum::Else,
355
0
        cmListFileContext::FromListFileFunction(func, this->FileName),
356
0
      };
357
0
    } else if (name == "endif") {
358
0
      if (!TopIs(stack, NestingStateEnum::If) &&
359
0
          !TopIs(stack, NestingStateEnum::Else)) {
360
0
        return cmListFileContext::FromListFileFunction(func, this->FileName);
361
0
      }
362
0
      stack.pop_back();
363
0
    } else if (name == "while") {
364
0
      stack.push_back({
365
0
        NestingStateEnum::While,
366
0
        cmListFileContext::FromListFileFunction(func, this->FileName),
367
0
      });
368
0
    } else if (name == "endwhile") {
369
0
      if (!TopIs(stack, NestingStateEnum::While)) {
370
0
        return cmListFileContext::FromListFileFunction(func, this->FileName);
371
0
      }
372
0
      stack.pop_back();
373
0
    } else if (name == "foreach") {
374
0
      stack.push_back({
375
0
        NestingStateEnum::Foreach,
376
0
        cmListFileContext::FromListFileFunction(func, this->FileName),
377
0
      });
378
0
    } else if (name == "endforeach") {
379
0
      if (!TopIs(stack, NestingStateEnum::Foreach)) {
380
0
        return cmListFileContext::FromListFileFunction(func, this->FileName);
381
0
      }
382
0
      stack.pop_back();
383
0
    } else if (name == "function") {
384
0
      stack.push_back({
385
0
        NestingStateEnum::Function,
386
0
        cmListFileContext::FromListFileFunction(func, this->FileName),
387
0
      });
388
0
    } else if (name == "endfunction") {
389
0
      if (!TopIs(stack, NestingStateEnum::Function)) {
390
0
        return cmListFileContext::FromListFileFunction(func, this->FileName);
391
0
      }
392
0
      stack.pop_back();
393
0
    } else if (name == "macro") {
394
0
      stack.push_back({
395
0
        NestingStateEnum::Macro,
396
0
        cmListFileContext::FromListFileFunction(func, this->FileName),
397
0
      });
398
0
    } else if (name == "endmacro") {
399
0
      if (!TopIs(stack, NestingStateEnum::Macro)) {
400
0
        return cmListFileContext::FromListFileFunction(func, this->FileName);
401
0
      }
402
0
      stack.pop_back();
403
0
    } else if (name == "block") {
404
0
      stack.push_back({
405
0
        NestingStateEnum::Block,
406
0
        cmListFileContext::FromListFileFunction(func, this->FileName),
407
0
      });
408
0
    } else if (name == "endblock") {
409
0
      if (!TopIs(stack, NestingStateEnum::Block)) {
410
0
        return cmListFileContext::FromListFileFunction(func, this->FileName);
411
0
      }
412
0
      stack.pop_back();
413
0
    }
414
0
  }
415
416
0
  if (!stack.empty()) {
417
0
    return stack.back().Context;
418
0
  }
419
420
0
  return cm::nullopt;
421
0
}
422
423
} // anonymous namespace
424
425
bool cmListFile::ParseFile(char const* filename, cmMessenger* messenger,
426
                           cmListFileBacktrace const& lfbt)
427
1
{
428
1
  if (!cmSystemTools::FileExists(filename) ||
429
1
      cmSystemTools::FileIsDirectory(filename)) {
430
0
    return false;
431
0
  }
432
433
1
  bool parseError = false;
434
435
1
  {
436
1
    cmListFileParser parser(this, lfbt, messenger);
437
1
    parseError = !parser.ParseFile(filename);
438
1
  }
439
440
1
  return !parseError;
441
1
}
442
443
bool cmListFile::ParseString(cm::string_view str, char const* virtual_filename,
444
                             cmMessenger* messenger,
445
                             cmListFileBacktrace const& lfbt)
446
0
{
447
0
  bool parseError = false;
448
449
0
  {
450
0
    cmListFileParser parser(this, lfbt, messenger);
451
0
    parseError = !parser.ParseString(str, virtual_filename);
452
0
  }
453
454
0
  return !parseError;
455
0
}
456
457
#include "cmStack.tcc"
458
template class cmStack<cmListFileContext const, cmListFileBacktrace,
459
                       cmStackType::Const>;
460
461
std::ostream& operator<<(std::ostream& os, cmListFileContext const& lfc)
462
1
{
463
1
  os << lfc.FilePath;
464
1
  if (lfc.Line > 0) {
465
1
    os << ':' << lfc.Line;
466
1
    if (!lfc.Name.empty()) {
467
0
      os << " (" << lfc.Name << ')';
468
0
    }
469
1
  } else if (lfc.Line == cmListFileContext::DeferPlaceholderLine) {
470
0
    os << ":DEFERRED";
471
0
  }
472
1
  return os;
473
1
}
474
475
bool operator<(cmListFileContext const& lhs, cmListFileContext const& rhs)
476
0
{
477
0
  if (lhs.Line != rhs.Line) {
478
0
    return lhs.Line < rhs.Line;
479
0
  }
480
0
  return lhs.FilePath < rhs.FilePath;
481
0
}
482
483
bool operator==(cmListFileContext const& lhs, cmListFileContext const& rhs)
484
0
{
485
0
  return lhs.Line == rhs.Line && lhs.FilePath == rhs.FilePath;
486
0
}
487
488
bool operator!=(cmListFileContext const& lhs, cmListFileContext const& rhs)
489
0
{
490
0
  return !(lhs == rhs);
491
0
}
492
493
std::ostream& operator<<(std::ostream& os, BT<std::string> const& s)
494
0
{
495
0
  return os << s.Value;
496
0
}
497
498
std::vector<BT<std::string>> cmExpandListWithBacktrace(
499
  std::string const& list, cmListFileBacktrace const& bt,
500
  cmList::EmptyElements emptyArgs)
501
0
{
502
0
  std::vector<BT<std::string>> result;
503
0
  cmList tmp{ list, emptyArgs };
504
0
  result.reserve(tmp.size());
505
0
  for (std::string& i : tmp) {
506
0
    result.emplace_back(std::move(i), bt);
507
0
  }
508
0
  return result;
509
0
}