Coverage Report

Created: 2026-09-14 06:43

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/CMake/Source/cmRST.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 "cmRST.h"
4
5
#include <algorithm>
6
#include <iterator>
7
#include <utility>
8
9
#include "cmsys/FStream.hxx"
10
#include "cmsys/String.h"
11
12
#include "cmAlgorithms.h"
13
#include "cmRange.h"
14
#include "cmStringAlgorithms.h"
15
#include "cmSystemTools.h"
16
#include "cmVersion.h"
17
18
cmRST::cmRST(std::string docroot)
19
0
  : DocRoot(std::move(docroot))
20
0
  , CMakeDirective("^.. (cmake:)?("
21
0
                   "command|diagnostic|envvar|genex|signature|variable"
22
0
                   ")::")
23
0
  , CMakeModuleDirective("^.. cmake-module::[ \t]+([^ \t\n]+)$")
24
0
  , ParsedLiteralDirective("^.. parsed-literal::[ \t]*(.*)$")
25
0
  , CodeBlockDirective("^.. code-block::[ \t]*(.*)$")
26
0
  , ReplaceDirective("^.. (\\|[^|]+\\|) replace::[ \t]*(.*)$")
27
0
  , IncludeDirective("^.. include::[ \t]+([^ \t\n]+)$")
28
0
  , TocTreeDirective("^.. toctree::[ \t]*(.*)$")
29
0
  , ProductionListDirective("^.. productionlist::[ \t]*(.*)$")
30
0
  , NoteDirective("^.. note::[ \t]*(.*)$")
31
0
  , VersionDirective("^.. version(added|changed)::[ \t]*(.*)$")
32
0
  , ModuleRST(R"(^#\[(=*)\[\.rst:$)")
33
0
  , CMakeRole("(:cmake)?:("
34
0
              "cref|"
35
0
              "program|command|cpack_gen|diagnostic|envvar|generator|genex|"
36
0
              "manual|module|policy|preset|variable|"
37
0
              "prop_cache|prop_dir|prop_fs|prop_gbl|prop_inst|prop_sf|"
38
0
              "prop_test|prop_tgt|"
39
0
              "option|cmake-option|cmake-build-option|cmake-install-option|"
40
0
              "cmake-workflow-option|cpack-option|ctest-option|"
41
0
              "ctest-dashboard-option"
42
0
              "):`(<*([^`<]|[^` \t]<)*)([ \t]+<[^`]*>)?`")
43
0
  , InlineLink("`(<*([^`<]|[^` \t]<)*)([ \t]+<[^`]*>)?`__?")
44
0
  , InlineLiteral("``([^`]*)``")
45
0
  , Substitution("(^|[^A-Za-z0-9_])"
46
0
                 "((\\|[^| \t\r\n]([^|\r\n]*[^| \t\r\n])?\\|)(__|_|))"
47
0
                 "([^A-Za-z0-9_]|$)")
48
0
  , TocTreeLink("^.*[ \t]+<([^>]+)>$")
49
0
  , Header(R"(^(#+|\*+|=+|-+|\^+|"+)$)")
50
0
{
51
0
  this->Replace.emplace_back();
52
0
  this->Replace[0]["|release|"] = cmVersion::GetCMakeVersion();
53
0
}
54
55
bool cmRST::ProcessFile(std::string const& fname, bool isModule)
56
0
{
57
0
  cmsys::ifstream fin(fname.c_str());
58
0
  if (fin) {
59
0
    this->DocDir = cmSystemTools::GetFilenamePath(fname);
60
0
    if (isModule) {
61
0
      this->ProcessModule(fin);
62
0
    } else {
63
0
      this->ProcessRST(fin);
64
0
    }
65
0
    this->OutputLinePending = true;
66
0
    return true;
67
0
  }
68
0
  return false;
69
0
}
70
71
void cmRST::ProcessRST(std::istream& is)
72
0
{
73
0
  std::string line;
74
0
  while (cmSystemTools::GetLineFromStream(is, line)) {
75
0
    this->ProcessLine(line);
76
0
  }
77
0
  this->Reset();
78
0
}
79
80
void cmRST::ProcessModule(std::istream& is)
81
0
{
82
0
  std::string line;
83
0
  std::string rst;
84
0
  while (cmSystemTools::GetLineFromStream(is, line)) {
85
0
    if (!rst.empty() && rst != "#") {
86
      // Bracket mode: check for end bracket
87
0
      std::string::size_type pos = line.find(rst);
88
0
      if (pos == std::string::npos) {
89
0
        this->ProcessLine(line);
90
0
      } else {
91
0
        if (line[0] != '#') {
92
0
          line.resize(pos);
93
0
          this->ProcessLine(line);
94
0
        }
95
0
        rst.clear();
96
0
        this->Reset();
97
0
        this->OutputLinePending = true;
98
0
      }
99
0
    } else {
100
      // Line mode: check for .rst start (bracket or line)
101
0
      if (rst == "#") {
102
0
        if (line == "#") {
103
0
          this->ProcessLine("");
104
0
          continue;
105
0
        }
106
0
        if (cmHasLiteralPrefix(line, "# ")) {
107
0
          line.erase(0, 2);
108
0
          this->ProcessLine(line);
109
0
          continue;
110
0
        }
111
0
        rst.clear();
112
0
        this->Reset();
113
0
        this->OutputLinePending = true;
114
0
      }
115
0
      if (line == "#.rst:") {
116
0
        rst = "#";
117
0
      } else if (this->ModuleRST.find(line)) {
118
0
        rst = "]" + this->ModuleRST.match(1) + "]";
119
0
      }
120
0
    }
121
0
  }
122
0
  if (rst == "#") {
123
0
    this->Reset();
124
0
  }
125
0
}
126
127
void cmRST::Reset()
128
0
{
129
0
  if (!this->MarkupLines.empty()) {
130
0
    cmRST::UnindentLines(this->MarkupLines);
131
0
  }
132
0
  switch (this->DirectiveType) {
133
0
    case Directive::None:
134
0
      break;
135
0
    case Directive::ParsedLiteral:
136
0
      this->ProcessDirectiveParsedLiteral();
137
0
      break;
138
0
    case Directive::LiteralBlock:
139
0
      this->ProcessDirectiveLiteralBlock();
140
0
      break;
141
0
    case Directive::CodeBlock:
142
0
      this->ProcessDirectiveCodeBlock();
143
0
      break;
144
0
    case Directive::Replace:
145
0
      this->ProcessDirectiveReplace();
146
0
      break;
147
0
    case Directive::TocTree:
148
0
      this->ProcessDirectiveTocTree();
149
0
      break;
150
0
  }
151
0
  this->MarkupType = Markup::None;
152
0
  this->DirectiveType = Directive::None;
153
0
  this->MarkupLines.clear();
154
0
}
155
156
void cmRST::ProcessLine(std::string const& line)
157
0
{
158
0
  bool lastLineEndedInColonColon = this->LastLineEndedInColonColon;
159
0
  this->LastLineEndedInColonColon = false;
160
161
  // A line starting in .. is an explicit markup start.
162
0
  if (line == ".." ||
163
0
      (line.size() >= 3 && line[0] == '.' && line[1] == '.' &&
164
0
       cmsysString_isspace(line[2]))) {
165
0
    this->Reset();
166
0
    this->MarkupType =
167
0
      (line.find_first_not_of(" \t", 2) == std::string::npos ? Markup::Empty
168
0
                                                             : Markup::Normal);
169
    // XXX(clang-tidy): https://bugs.llvm.org/show_bug.cgi?id=44165
170
    // NOLINTNEXTLINE(bugprone-branch-clone)
171
0
    if (this->CMakeDirective.find(line)) {
172
      // Output cmake domain directives and their content normally.
173
0
      this->NormalLine(line);
174
0
    } else if (this->CMakeModuleDirective.find(line)) {
175
      // Process cmake-module directive: scan .cmake file comments.
176
0
      std::string file = this->CMakeModuleDirective.match(1);
177
0
      if (file.empty() || !this->ProcessInclude(file, Include::Module)) {
178
0
        this->NormalLine(line);
179
0
      }
180
0
    } else if (this->ParsedLiteralDirective.find(line)) {
181
      // Record the literal lines to output after whole block.
182
0
      this->DirectiveType = Directive::ParsedLiteral;
183
0
      this->MarkupLines.push_back(this->ParsedLiteralDirective.match(1));
184
0
    } else if (this->CodeBlockDirective.find(line)) {
185
      // Record the literal lines to output after whole block.
186
      // Ignore the language spec and record the opening line as blank.
187
0
      this->DirectiveType = Directive::CodeBlock;
188
0
      this->MarkupLines.emplace_back();
189
0
    } else if (this->ReplaceDirective.find(line)) {
190
      // Record the replace directive content.
191
0
      this->DirectiveType = Directive::Replace;
192
0
      this->ReplaceName = this->ReplaceDirective.match(1);
193
0
      this->MarkupLines.push_back(this->ReplaceDirective.match(2));
194
0
    } else if (this->IncludeDirective.find(line)) {
195
      // Process the include directive or output the directive and its
196
      // content normally if it fails.
197
0
      std::string file = this->IncludeDirective.match(1);
198
0
      if (file.empty() || !this->ProcessInclude(file, Include::Normal)) {
199
0
        this->NormalLine(line);
200
0
      }
201
0
    } else if (this->TocTreeDirective.find(line)) {
202
      // Record the toctree entries to process after whole block.
203
0
      this->DirectiveType = Directive::TocTree;
204
0
      this->MarkupLines.push_back(this->TocTreeDirective.match(1));
205
0
    } else if (this->ProductionListDirective.find(line)) {
206
      // Output productionlist directives and their content normally.
207
0
      this->NormalLine(line);
208
0
    } else if (this->NoteDirective.find(line)) {
209
      // Output note directives and their content normally.
210
0
      this->NormalLine(line);
211
0
    } else if (this->VersionDirective.find(line)) {
212
      // Output versionadded and versionchanged directives and their content
213
      // normally.
214
0
      this->NormalLine(line);
215
0
    }
216
0
  }
217
  // An explicit markup start followed by nothing but whitespace and a
218
  // blank line does not consume any indented text following.
219
0
  else if (this->MarkupType == Markup::Empty && line.empty()) {
220
0
    this->NormalLine(line);
221
0
  }
222
  // Indented lines following an explicit markup start are explicit markup.
223
0
  else if (this->MarkupType != Markup::None &&
224
0
           (line.empty() || cmsysString_isspace(line[0]))) {
225
0
    this->MarkupType = Markup::Normal;
226
    // Record markup lines if the start line was recorded.
227
0
    if (!this->MarkupLines.empty()) {
228
0
      this->MarkupLines.push_back(line);
229
0
    }
230
0
  }
231
  // A blank line following a paragraph ending in "::" starts a literal block.
232
0
  else if (lastLineEndedInColonColon && line.empty()) {
233
    // Record the literal lines to output after whole block.
234
0
    this->MarkupType = Markup::Normal;
235
0
    this->DirectiveType = Directive::LiteralBlock;
236
0
    this->MarkupLines.emplace_back();
237
0
    this->OutputLine({}, false);
238
0
  }
239
  // Print non-markup lines.
240
0
  else {
241
0
    this->NormalLine(line);
242
0
    this->LastLineEndedInColonColon =
243
0
      (line.size() >= 2 && line[line.size() - 2] == ':' && line.back() == ':');
244
0
  }
245
0
}
246
247
void cmRST::NormalLine(std::string const& line)
248
0
{
249
0
  this->Reset();
250
0
  this->OutputLine(line, true);
251
0
}
252
253
void cmRST::OutputLine(std::string const& line, bool inlineMarkup)
254
0
{
255
0
  if (this->OutputLinePending) {
256
0
    this->OutputLines.emplace_back(std::string{}, 0, false);
257
0
    this->OutputLinePending = false;
258
0
  }
259
0
  this->OutputLines.emplace_back(line, this->ContextOffset, inlineMarkup);
260
0
}
261
262
void cmRST::Write(std::ostream& os)
263
0
{
264
0
  for (ContentLine const& c : this->OutputLines) {
265
0
    this->WriteLine(os, c);
266
0
  }
267
0
}
268
269
void cmRST::WriteLine(std::ostream& os, ContentLine const& content)
270
0
{
271
0
  if (content.InlineMarkup) {
272
0
    if (this->LastLength && this->Header.find(content.Content.c_str())) {
273
0
      os << std::string(this->LastLength, content.Content[0]) << '\n';
274
0
      return;
275
0
    }
276
277
0
    this->LastLength = 0;
278
0
    std::string line =
279
0
      this->ReplaceSubstitutions(content.Content, content.Context);
280
0
    std::string::size_type pos = 0;
281
0
    for (;;) {
282
0
      std::string::size_type* first = nullptr;
283
0
      std::string::size_type role_start = std::string::npos;
284
0
      std::string::size_type link_start = std::string::npos;
285
0
      std::string::size_type lit_start = std::string::npos;
286
0
      if (this->CMakeRole.find(line.c_str() + pos)) {
287
0
        role_start = this->CMakeRole.start();
288
0
        first = &role_start;
289
0
      }
290
0
      if (this->InlineLiteral.find(line.c_str() + pos)) {
291
0
        lit_start = this->InlineLiteral.start();
292
0
        if (!first || lit_start < *first) {
293
0
          first = &lit_start;
294
0
        }
295
0
      }
296
0
      if (this->InlineLink.find(line.c_str() + pos)) {
297
0
        link_start = this->InlineLink.start();
298
0
        if (!first || link_start < *first) {
299
0
          first = &link_start;
300
0
        }
301
0
      }
302
0
      if (first == &role_start) {
303
0
        this->LastLength += role_start;
304
0
        os << line.substr(pos, role_start);
305
0
        std::string text = this->CMakeRole.match(3);
306
        // If a command reference has no explicit target and
307
        // no explicit "(...)" then add "()" to the text.
308
0
        if (this->CMakeRole.match(2) == "command" &&
309
0
            this->CMakeRole.match(5).empty() &&
310
0
            text.find_first_of("()") == std::string::npos) {
311
0
          text += "()";
312
0
        }
313
0
        this->LastLength += 4 + text.size();
314
0
        os << "``" << text << "``";
315
0
        pos += this->CMakeRole.end();
316
0
      } else if (first == &lit_start) {
317
0
        this->LastLength += lit_start;
318
0
        os << line.substr(pos, lit_start);
319
0
        std::string text = this->InlineLiteral.match(1);
320
0
        pos += this->InlineLiteral.end();
321
0
        this->LastLength += 4 + text.size();
322
0
        os << "``" << text << "``";
323
0
      } else if (first == &link_start) {
324
0
        this->LastLength += link_start;
325
0
        os << line.substr(pos, link_start);
326
0
        std::string text = this->InlineLink.match(1);
327
0
        bool escaped = false;
328
0
        for (char c : text) {
329
0
          if (escaped) {
330
0
            escaped = false;
331
0
            ++this->LastLength;
332
0
            os << c;
333
0
          } else if (c == '\\') {
334
0
            escaped = true;
335
0
          } else {
336
0
            ++this->LastLength;
337
0
            os << c;
338
0
          }
339
0
        }
340
0
        pos += this->InlineLink.end();
341
0
      } else {
342
0
        break;
343
0
      }
344
0
    }
345
0
    this->LastLength += line.size() - pos;
346
0
    os << line.substr(pos) << '\n';
347
0
  } else {
348
0
    this->LastLength = content.Content.size();
349
0
    os << content.Content << '\n';
350
0
  }
351
0
}
352
353
std::string cmRST::ReplaceSubstitutions(std::string const& line,
354
                                        size_t context)
355
0
{
356
0
  std::string out;
357
0
  std::string::size_type pos = 0;
358
0
  while (this->Substitution.find(line.c_str() + pos)) {
359
0
    std::string::size_type start = this->Substitution.start(2);
360
0
    std::string::size_type end = this->Substitution.end(2);
361
0
    std::string substitute = this->Substitution.match(3);
362
0
    auto replace = this->Replace[context].find(substitute);
363
0
    if (replace != this->Replace[context].end()) {
364
0
      std::pair<std::set<std::string>::iterator, bool> replaced =
365
0
        this->Replaced.insert(substitute);
366
0
      if (replaced.second) {
367
0
        substitute = this->ReplaceSubstitutions(replace->second, context);
368
0
        this->Replaced.erase(replaced.first);
369
0
      }
370
0
    }
371
0
    out += line.substr(pos, start);
372
0
    out += substitute;
373
0
    pos += end;
374
0
  }
375
0
  out += line.substr(pos);
376
0
  return out;
377
0
}
378
379
void cmRST::OutputMarkupLines(bool inlineMarkup)
380
0
{
381
0
  for (auto line : this->MarkupLines) {
382
0
    if (!line.empty()) {
383
0
      line = cmStrCat(' ', line);
384
0
    }
385
0
    this->OutputLine(line, inlineMarkup);
386
0
  }
387
0
  this->OutputLinePending = true;
388
0
}
389
390
bool cmRST::ProcessInclude(std::string file, Include type)
391
0
{
392
0
  bool found = false;
393
0
  if (this->IncludeDepth < 10) {
394
0
    cmRST r(this->DocRoot);
395
0
    r.IncludeDepth = this->IncludeDepth + 1;
396
0
    r.OutputLinePending = this->OutputLinePending;
397
0
    if (type == Include::TocTree) {
398
0
      r.ContextOffset = this->ContextOffset + this->Replace.size();
399
0
    } else {
400
0
      r.ContextOffset = this->ContextOffset;
401
0
      r.Replace = this->Replace;
402
0
    }
403
0
    if (file[0] == '/') {
404
0
      file = this->DocRoot + file;
405
0
    } else {
406
0
      file = this->DocDir + "/" + file;
407
0
    }
408
0
    found = r.ProcessFile(file, type == Include::Module);
409
0
    if (type == Include::TocTree) {
410
0
      this->Replace.insert(this->Replace.end(),
411
0
                           std::make_move_iterator(r.Replace.begin()),
412
0
                           std::make_move_iterator(r.Replace.end()));
413
0
    } else {
414
0
      this->Replace = std::move(r.Replace);
415
0
    }
416
0
    this->OutputLines.insert(this->OutputLines.end(),
417
0
                             std::make_move_iterator(r.OutputLines.begin()),
418
0
                             std::make_move_iterator(r.OutputLines.end()));
419
0
    this->OutputLinePending = r.OutputLinePending;
420
0
  }
421
0
  return found;
422
0
}
423
424
void cmRST::ProcessDirectiveParsedLiteral()
425
0
{
426
0
  this->OutputMarkupLines(true);
427
0
}
428
429
void cmRST::ProcessDirectiveLiteralBlock()
430
0
{
431
0
  this->OutputMarkupLines(false);
432
0
}
433
434
void cmRST::ProcessDirectiveCodeBlock()
435
0
{
436
0
  this->OutputMarkupLines(false);
437
0
}
438
439
void cmRST::ProcessDirectiveReplace()
440
0
{
441
  // Record markup lines as replacement text.
442
0
  std::string& replacement = this->Replace[0][this->ReplaceName];
443
0
  replacement += cmJoin(this->MarkupLines, " ");
444
0
  this->ReplaceName.clear();
445
0
}
446
447
void cmRST::ProcessDirectiveTocTree()
448
0
{
449
  // Process documents referenced by toctree directive.
450
0
  for (std::string const& line : this->MarkupLines) {
451
0
    if (!line.empty() && line[0] != ':') {
452
0
      if (this->TocTreeLink.find(line)) {
453
0
        std::string const& link = this->TocTreeLink.match(1);
454
0
        this->ProcessInclude(link + ".rst", Include::TocTree);
455
0
      } else {
456
0
        this->ProcessInclude(line + ".rst", Include::TocTree);
457
0
      }
458
0
    }
459
0
  }
460
0
}
461
462
void cmRST::UnindentLines(std::vector<std::string>& lines)
463
0
{
464
  // Remove the common indentation from the second and later lines.
465
0
  std::string indentText;
466
0
  std::string::size_type indentEnd = 0;
467
0
  bool first = true;
468
0
  for (size_t i = 1; i < lines.size(); ++i) {
469
0
    std::string const& line = lines[i];
470
471
    // Do not consider empty lines.
472
0
    if (line.empty()) {
473
0
      continue;
474
0
    }
475
476
    // Record indentation on first non-empty line.
477
0
    if (first) {
478
0
      first = false;
479
0
      indentEnd = line.find_first_not_of(" \t");
480
0
      indentText = line.substr(0, indentEnd);
481
0
      continue;
482
0
    }
483
484
    // Truncate indentation to match that on this line.
485
0
    indentEnd = std::min(indentEnd, line.size());
486
0
    for (std::string::size_type j = 0; j != indentEnd; ++j) {
487
0
      if (line[j] != indentText[j]) {
488
0
        indentEnd = j;
489
0
        break;
490
0
      }
491
0
    }
492
0
  }
493
494
  // Update second and later lines.
495
0
  for (size_t i = 1; i < lines.size(); ++i) {
496
0
    std::string& line = lines[i];
497
0
    if (!line.empty()) {
498
0
      line = line.substr(indentEnd);
499
0
    }
500
0
  }
501
502
0
  auto it = lines.cbegin();
503
0
  size_t leadingEmpty = std::distance(it, cmFindNot(lines, std::string()));
504
505
0
  auto rit = lines.crbegin();
506
0
  size_t trailingEmpty =
507
0
    std::distance(rit, cmFindNot(cmReverseRange(lines), std::string()));
508
509
0
  if ((leadingEmpty + trailingEmpty) >= lines.size()) {
510
    // All lines are empty.  The markup block is empty.  Leave only one.
511
0
    lines.resize(1);
512
0
    return;
513
0
  }
514
515
0
  auto contentEnd = cmRotate(lines.begin(), lines.begin() + leadingEmpty,
516
0
                             lines.end() - trailingEmpty);
517
0
  lines.erase(contentEnd, lines.end());
518
0
}