mirror of
https://gitlab.kitware.com/cmake/cmake.git
synced 2026-09-25 04:09:36 +03:00
Merge topic 'genex-list-sort-comparator'
4d86ef5e86GenEx: add $<LIST:SORT,...,COMPARATOR,body> comparatorc418a3dc85GenEx: factor out reusable SORT comparator and option-parsing helperse335872625GenEx: generalize bound operands to a frame and add $<_1> Acked-by: Kitware Robot <kwrobot@kitware.com> Tested-by: buildbot <buildbot@kitware.com> Merge-request: !12228
This commit is contained in:
@@ -1098,6 +1098,24 @@ List Ordering
|
||||
|
||||
$<LIST:SORT,list,CASE:SENSITIVE,COMPARE:STRING,ORDER:DESCENDING>
|
||||
|
||||
.. versionadded:: 4.5
|
||||
|
||||
A ``COMPARATOR`` option sorts using a generator-expression ``body`` instead
|
||||
of a built-in ordering:
|
||||
|
||||
.. code-block:: cmake
|
||||
|
||||
$<LIST:SORT,list,COMPARATOR,body[,ORDER:ASCENDING|DESCENDING][,CASE:SENSITIVE|INSENSITIVE]>
|
||||
|
||||
``body`` is evaluated once per comparison with the two items being compared
|
||||
bound to :genex:`$<_0>` and :genex:`$<_1>`. It must evaluate to exactly
|
||||
``0`` or ``1``; ``1`` means :genex:`$<_0>` sorts before :genex:`$<_1>`.
|
||||
``COMPARATOR`` is incompatible with ``COMPARE:``. ``ORDER:DESCENDING``
|
||||
reverses the comparator and ``CASE:INSENSITIVE`` case-folds the values the
|
||||
body sees, both as in the configure-time :command:`list(SORT)`. The body
|
||||
must induce a `strict weak ordering
|
||||
<https://en.wikipedia.org/wiki/Weak_ordering#Strict_weak_orderings>`_.
|
||||
|
||||
.. _GenEx Bound Operands:
|
||||
|
||||
Bound Operands
|
||||
@@ -1117,6 +1135,20 @@ Bound Operands
|
||||
``$<_0>`` is only valid inside the body of a binding operation. Using it
|
||||
anywhere else is an error.
|
||||
|
||||
.. genex:: $<_1>
|
||||
|
||||
.. versionadded:: 4.5
|
||||
|
||||
The second *bound operand* of a *binding operation* that binds at least two
|
||||
operands, expanding to the second supplied value.
|
||||
|
||||
For example, :genex:`$<LIST:SORT,...,COMPARATOR,body>` evaluates ``body``
|
||||
once per comparison with :genex:`$<_0>` and ``$<_1>`` bound to the two items
|
||||
being compared.
|
||||
|
||||
``$<_1>`` is only valid inside the body of a binding operation that binds at
|
||||
least two operands. Using it anywhere else is an error.
|
||||
|
||||
Path Expressions
|
||||
----------------
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
genex-list-sort-comparator
|
||||
--------------------------
|
||||
|
||||
* The :genex:`LIST` generator expression's ``SORT`` operation gained a
|
||||
``COMPARATOR`` option that orders items using an arbitrary generator
|
||||
expression evaluated once per comparison, with ``$<_0>`` and ``$<_1>``
|
||||
referring to the two items being compared.
|
||||
+20
-8
@@ -2,8 +2,10 @@
|
||||
file LICENSE.rst or https://cmake.org/licensing for details. */
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include <cm/optional>
|
||||
|
||||
@@ -26,26 +28,36 @@ struct Context final
|
||||
void SetCMP0189(cmPolicies::PolicyStatus cmp0189);
|
||||
cmPolicies::PolicyStatus GetCMP0189() const;
|
||||
|
||||
void SetBoundOperands(std::vector<std::string> operands);
|
||||
void SetBoundOperand(std::string value);
|
||||
bool HasBoundOperand() const;
|
||||
std::string const& GetBoundOperand() const;
|
||||
std::size_t BoundOperandCount() const;
|
||||
bool HasBoundOperand(std::size_t index = 0) const;
|
||||
std::string const& GetBoundOperand(std::size_t index = 0) const;
|
||||
|
||||
private:
|
||||
cm::optional<cmPolicies::PolicyStatus> CMP0189;
|
||||
cm::optional<std::string> BoundOperand;
|
||||
std::vector<std::string> BoundOperands;
|
||||
};
|
||||
|
||||
inline void Context::SetBoundOperands(std::vector<std::string> operands)
|
||||
{
|
||||
this->BoundOperands = std::move(operands);
|
||||
}
|
||||
inline void Context::SetBoundOperand(std::string value)
|
||||
{
|
||||
this->BoundOperand = std::move(value);
|
||||
this->SetBoundOperands({ std::move(value) });
|
||||
}
|
||||
inline bool Context::HasBoundOperand() const
|
||||
inline std::size_t Context::BoundOperandCount() const
|
||||
{
|
||||
return this->BoundOperand.has_value();
|
||||
return this->BoundOperands.size();
|
||||
}
|
||||
inline std::string const& Context::GetBoundOperand() const
|
||||
inline bool Context::HasBoundOperand(std::size_t index) const
|
||||
{
|
||||
return *this->BoundOperand;
|
||||
return index < this->BoundOperandCount();
|
||||
}
|
||||
inline std::string const& Context::GetBoundOperand(std::size_t index) const
|
||||
{
|
||||
return this->BoundOperands[index];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -107,17 +107,17 @@ std::string cmGeneratorExpressionNode::EvaluateDependentExpression(
|
||||
return result;
|
||||
}
|
||||
|
||||
// Re-evaluate the unevaluated <body> subtree of a binding operation with
|
||||
// `$<_0>` bound to the given operand. A fresh Evaluation is built from a
|
||||
// copied, mutated Context so that nested binding operations can shadow `$<_0>`
|
||||
// and restore it on exit.
|
||||
static std::string EvaluateBodyWithBoundOperand(
|
||||
// Re-evaluate the unevaluated <body> subtree of a binding operation with the
|
||||
// given operands bound (accessible as $<_0>, $<_1>, ...). A fresh Evaluation
|
||||
// is built from a copied, mutated Context so that nested binding operations
|
||||
// can shadow the operands and restore them on exit.
|
||||
static std::string EvaluateBodyWithBoundOperands(
|
||||
cmGeneratorExpressionEvaluatorVector const& bodyExpr,
|
||||
std::string const& operand, cm::GenEx::Evaluation* eval,
|
||||
std::vector<std::string> operands, cm::GenEx::Evaluation* eval,
|
||||
cmGeneratorExpressionDAGChecker* dagChecker)
|
||||
{
|
||||
cm::GenEx::Context elemContext = eval->Context; // copy
|
||||
elemContext.SetBoundOperand(operand);
|
||||
elemContext.SetBoundOperands(std::move(operands));
|
||||
cm::GenEx::Evaluation elemEval(
|
||||
elemContext, eval->Quiet, eval->HeadTarget, eval->CurrentTarget,
|
||||
eval->EvaluateForBuildsystem, eval->Backtrace);
|
||||
@@ -149,6 +149,15 @@ static std::string EvaluateBodyWithBoundOperand(
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::string EvaluateBodyWithBoundOperand(
|
||||
cmGeneratorExpressionEvaluatorVector const& bodyExpr,
|
||||
std::string const& operand, cm::GenEx::Evaluation* eval,
|
||||
cmGeneratorExpressionDAGChecker* dagChecker)
|
||||
{
|
||||
return EvaluateBodyWithBoundOperands(bodyExpr, { operand }, eval,
|
||||
dagChecker);
|
||||
}
|
||||
|
||||
// Evaluate `predicateBody` once per element of `list`, binding `$<_0>` to the
|
||||
// element (reusing EvaluateBodyWithBoundOperand). Each result must be exactly
|
||||
// "0" or "1". Returns the per-element boolean mask, or cm::nullopt after
|
||||
@@ -220,9 +229,12 @@ static const struct OneNode : public cmGeneratorExpressionNode
|
||||
}
|
||||
} oneNode;
|
||||
|
||||
static const struct BoundOperandNode : public cmGeneratorExpressionNode
|
||||
struct BoundOperandNode : public cmGeneratorExpressionNode
|
||||
{
|
||||
BoundOperandNode() {} // NOLINT(modernize-use-equals-default)
|
||||
explicit BoundOperandNode(std::size_t index)
|
||||
: Index(index)
|
||||
{
|
||||
}
|
||||
|
||||
int NumExpectedParameters() const override { return 0; }
|
||||
|
||||
@@ -231,15 +243,31 @@ static const struct BoundOperandNode : public cmGeneratorExpressionNode
|
||||
cm::GenEx::Evaluation* eval, GeneratorExpressionContent const* content,
|
||||
cmGeneratorExpressionDAGChecker* /*dagChecker*/) const override
|
||||
{
|
||||
if (!eval->Context.HasBoundOperand()) {
|
||||
reportError(eval, content->GetOriginalExpression(),
|
||||
"$<_0> may only be used inside the body of a binding "
|
||||
"operation.");
|
||||
if (!eval->Context.HasBoundOperand(this->Index)) {
|
||||
std::size_t const count = eval->Context.BoundOperandCount();
|
||||
if (count == 0) {
|
||||
reportError(eval, content->GetOriginalExpression(),
|
||||
cmStrCat("$<_", this->Index,
|
||||
"> may only be used inside the body of a binding "
|
||||
"operation."));
|
||||
} else {
|
||||
reportError(
|
||||
eval, content->GetOriginalExpression(),
|
||||
cmStrCat(
|
||||
"$<_", this->Index,
|
||||
"> is out of range for the current binding operation, which "
|
||||
"binds only ",
|
||||
count, " operand(s) (maximum $<_", count - 1, ">)."));
|
||||
}
|
||||
return std::string();
|
||||
}
|
||||
return eval->Context.GetBoundOperand();
|
||||
return eval->Context.GetBoundOperand(this->Index);
|
||||
}
|
||||
} boundOperandNode;
|
||||
|
||||
std::size_t Index;
|
||||
};
|
||||
static BoundOperandNode const boundOperandNode0{ 0 };
|
||||
static BoundOperandNode const boundOperandNode1{ 1 };
|
||||
|
||||
static const struct OneNode buildInterfaceNode;
|
||||
|
||||
@@ -2102,6 +2130,178 @@ std::string EvaluateTransformPredicate(
|
||||
}
|
||||
}
|
||||
|
||||
enum class SortOptionResult
|
||||
{
|
||||
NotRecognized, // `arg` is not a SORT option keyword
|
||||
Parsed, // recognized and applied to `sortConfig`
|
||||
Error, // recognized but malformed or duplicate (already reported)
|
||||
};
|
||||
|
||||
// Parse one $<LIST:SORT> colon-option (COMPARE:/CASE:/ORDER:) into sortConfig.
|
||||
SortOptionResult ParseSortOption(std::string const& arg,
|
||||
cmList::SortConfiguration& sortConfig,
|
||||
cm::GenEx::Evaluation* eval,
|
||||
GeneratorExpressionContent const* content)
|
||||
{
|
||||
using SortConfig = cmList::SortConfiguration;
|
||||
auto const COMPARE = "COMPARE:"_s;
|
||||
auto const CASE = "CASE:"_s;
|
||||
auto const ORDER = "ORDER:"_s;
|
||||
|
||||
if (cmHasPrefix(arg, COMPARE)) {
|
||||
if (sortConfig.Compare != SortConfig::CompareMethod::DEFAULT) {
|
||||
reportError(eval, content->GetOriginalExpression(),
|
||||
"sub-command SORT, COMPARE option has been specified "
|
||||
"multiple times.");
|
||||
return SortOptionResult::Error;
|
||||
}
|
||||
auto option = cm::string_view{ arg.c_str() + COMPARE.length() };
|
||||
if (option == "STRING"_s) {
|
||||
sortConfig.Compare = SortConfig::CompareMethod::STRING;
|
||||
} else if (option == "FILE_BASENAME"_s) {
|
||||
sortConfig.Compare = SortConfig::CompareMethod::FILE_BASENAME;
|
||||
} else if (option == "NATURAL"_s) {
|
||||
sortConfig.Compare = SortConfig::CompareMethod::NATURAL;
|
||||
} else {
|
||||
reportError(eval, content->GetOriginalExpression(),
|
||||
cmStrCat("sub-command SORT, an invalid COMPARE option has "
|
||||
"been specified: \"",
|
||||
option, "\"."));
|
||||
return SortOptionResult::Error;
|
||||
}
|
||||
return SortOptionResult::Parsed;
|
||||
}
|
||||
|
||||
if (cmHasPrefix(arg, CASE)) {
|
||||
if (sortConfig.Case != SortConfig::CaseSensitivity::DEFAULT) {
|
||||
reportError(eval, content->GetOriginalExpression(),
|
||||
"sub-command SORT, CASE option has been specified multiple "
|
||||
"times.");
|
||||
return SortOptionResult::Error;
|
||||
}
|
||||
auto option = cm::string_view{ arg.c_str() + CASE.length() };
|
||||
if (option == "SENSITIVE"_s) {
|
||||
sortConfig.Case = SortConfig::CaseSensitivity::SENSITIVE;
|
||||
} else if (option == "INSENSITIVE"_s) {
|
||||
sortConfig.Case = SortConfig::CaseSensitivity::INSENSITIVE;
|
||||
} else {
|
||||
reportError(eval, content->GetOriginalExpression(),
|
||||
cmStrCat("sub-command SORT, an invalid CASE option has been "
|
||||
"specified: \"",
|
||||
option, "\"."));
|
||||
return SortOptionResult::Error;
|
||||
}
|
||||
return SortOptionResult::Parsed;
|
||||
}
|
||||
|
||||
if (cmHasPrefix(arg, ORDER)) {
|
||||
if (sortConfig.Order != SortConfig::OrderMode::DEFAULT) {
|
||||
reportError(eval, content->GetOriginalExpression(),
|
||||
"sub-command SORT, ORDER option has been specified multiple "
|
||||
"times.");
|
||||
return SortOptionResult::Error;
|
||||
}
|
||||
auto option = cm::string_view{ arg.c_str() + ORDER.length() };
|
||||
if (option == "ASCENDING"_s) {
|
||||
sortConfig.Order = SortConfig::OrderMode::ASCENDING;
|
||||
} else if (option == "DESCENDING"_s) {
|
||||
sortConfig.Order = SortConfig::OrderMode::DESCENDING;
|
||||
} else {
|
||||
reportError(
|
||||
eval, content->GetOriginalExpression(),
|
||||
cmStrCat("sub-command SORT, an invalid ORDER option has been "
|
||||
"specified: \"",
|
||||
option, "\"."));
|
||||
return SortOptionResult::Error;
|
||||
}
|
||||
return SortOptionResult::Parsed;
|
||||
}
|
||||
|
||||
return SortOptionResult::NotRecognized;
|
||||
}
|
||||
|
||||
// $<LIST:SORT,...,COMPARATOR,body>: sort with a per-comparison genex body, the
|
||||
// two elements bound to $<_0> and $<_1>; body must yield "0" or "1".
|
||||
std::string EvaluateSortComparator(std::vector<std::string> const& parameters,
|
||||
std::size_t comparatorIndex,
|
||||
cm::GenEx::Evaluation* eval,
|
||||
GeneratorExpressionContent const* content,
|
||||
cmGeneratorExpressionDAGChecker* dagChecker)
|
||||
{
|
||||
if (comparatorIndex + 1 >= parameters.size()) {
|
||||
reportError(eval, content->GetOriginalExpression(),
|
||||
"sub-command SORT, COMPARATOR expects a <body> argument.");
|
||||
return std::string();
|
||||
}
|
||||
cmGeneratorExpressionEvaluatorVector const& bodyExpr =
|
||||
content->GetParamChildren()[comparatorIndex + 1];
|
||||
|
||||
using SortConfig = cmList::SortConfiguration;
|
||||
SortConfig sortConfig;
|
||||
sortConfig.Compare = SortConfig::CompareMethod::COMPARATOR;
|
||||
for (std::size_t i = 2; i < parameters.size(); ++i) {
|
||||
if (i == comparatorIndex || i == comparatorIndex + 1) {
|
||||
continue; // COMPARATOR keyword + its (empty) body slot
|
||||
}
|
||||
std::string const& arg = parameters[i];
|
||||
// COMPARATOR defines the ordering, so reject COMPARE:; CASE:/ORDER: are
|
||||
// accepted as in list(SORT ... COMPARATOR) (CASE: folds the body
|
||||
// operands).
|
||||
if (cmHasPrefix(arg, "COMPARE:"_s)) {
|
||||
reportError(eval, content->GetOriginalExpression(),
|
||||
"sub-command SORT, option \"COMPARE\" is incompatible with "
|
||||
"\"COMPARATOR\".");
|
||||
return std::string();
|
||||
}
|
||||
switch (ParseSortOption(arg, sortConfig, eval, content)) {
|
||||
case SortOptionResult::Parsed:
|
||||
break;
|
||||
case SortOptionResult::Error:
|
||||
return std::string();
|
||||
case SortOptionResult::NotRecognized:
|
||||
reportError(
|
||||
eval, content->GetOriginalExpression(),
|
||||
cmStrCat("sub-command SORT, option \"", arg, "\" is invalid."));
|
||||
return std::string();
|
||||
}
|
||||
}
|
||||
|
||||
cmList list = GetList(parameters[1]);
|
||||
if (list.size() < 2) {
|
||||
return list.to_string();
|
||||
}
|
||||
|
||||
// The strict-weak-ordering guard in cmList::sort may call this twice per
|
||||
// pair, so the body can be evaluated up to twice per comparison.
|
||||
auto comparator = [&](std::string const& a, std::string const& b) -> bool {
|
||||
std::string r =
|
||||
EvaluateBodyWithBoundOperands(bodyExpr, { a, b }, eval, dagChecker);
|
||||
if (eval->HadError) {
|
||||
throw cmList::transform_error(std::string{}); // body already reported
|
||||
}
|
||||
if (r == "1") {
|
||||
return true;
|
||||
}
|
||||
if (r == "0") {
|
||||
return false;
|
||||
}
|
||||
throw cmList::transform_error(
|
||||
cmStrCat("sub-command SORT, COMPARATOR body must evaluate to \"0\" or "
|
||||
"\"1\", but evaluated to \"",
|
||||
r, "\"."));
|
||||
};
|
||||
|
||||
try {
|
||||
list.sort(sortConfig, comparator);
|
||||
} catch (std::invalid_argument& e) {
|
||||
if (!eval->HadError) {
|
||||
reportError(eval, content->GetOriginalExpression(), e.what());
|
||||
}
|
||||
return std::string();
|
||||
}
|
||||
return list.to_string();
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -2245,6 +2445,12 @@ static const struct ListNode : public cmGeneratorExpressionNode
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// Leave the SORT COMPARATOR <body> unevaluated; a bare COMPARATOR token is
|
||||
// unambiguous since SORT's other options are colon-style.
|
||||
if (parameters.size() >= 3 && parameters[0] == "SORT" &&
|
||||
parameters.back() == "COMPARATOR") {
|
||||
return false;
|
||||
}
|
||||
// Skip the APPLY <body> (4th parameter) so $<_0> is not evaluated unbound;
|
||||
// selector args (5th+) evaluate normally.
|
||||
return !(parameters.size() == 3 && parameters[0] == "TRANSFORM" &&
|
||||
@@ -2364,6 +2570,17 @@ static const struct ListNode : public cmGeneratorExpressionNode
|
||||
.to_string();
|
||||
}
|
||||
|
||||
// SORT COMPARATOR is handled here, not the listCommands SORT lambda,
|
||||
// because the body needs the DAG checker.
|
||||
if (parameters.size() >= 3 && parameters[0] == "SORT") {
|
||||
for (std::size_t i = 2; i < parameters.size(); ++i) {
|
||||
if (parameters[i] == "COMPARATOR") {
|
||||
return EvaluateSortComparator(parameters, i, eval, content,
|
||||
dagChecker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static std::unordered_map<
|
||||
cm::string_view,
|
||||
std::function<std::string(cm::GenEx::Evaluation*,
|
||||
@@ -2678,97 +2895,19 @@ static const struct ListNode : public cmGeneratorExpressionNode
|
||||
false)) {
|
||||
auto list = GetList(args.front());
|
||||
args.advance(1);
|
||||
auto const COMPARE = "COMPARE:"_s;
|
||||
auto const CASE = "CASE:"_s;
|
||||
auto const ORDER = "ORDER:"_s;
|
||||
using SortConfig = cmList::SortConfiguration;
|
||||
SortConfig sortConfig;
|
||||
cmList::SortConfiguration sortConfig;
|
||||
for (auto const& arg : args) {
|
||||
if (cmHasPrefix(arg, COMPARE)) {
|
||||
if (sortConfig.Compare !=
|
||||
SortConfig::CompareMethod::DEFAULT) {
|
||||
reportError(ev, cnt->GetOriginalExpression(),
|
||||
"sub-command SORT, COMPARE option has been "
|
||||
"specified multiple times.");
|
||||
switch (ParseSortOption(arg, sortConfig, ev, cnt)) {
|
||||
case SortOptionResult::Parsed:
|
||||
break;
|
||||
case SortOptionResult::Error:
|
||||
return std::string{};
|
||||
}
|
||||
auto option =
|
||||
cm::string_view{ arg.c_str() + COMPARE.length() };
|
||||
if (option == "STRING"_s) {
|
||||
sortConfig.Compare = SortConfig::CompareMethod::STRING;
|
||||
continue;
|
||||
}
|
||||
if (option == "FILE_BASENAME"_s) {
|
||||
sortConfig.Compare =
|
||||
SortConfig::CompareMethod::FILE_BASENAME;
|
||||
continue;
|
||||
}
|
||||
if (option == "NATURAL"_s) {
|
||||
sortConfig.Compare = SortConfig::CompareMethod::NATURAL;
|
||||
continue;
|
||||
}
|
||||
reportError(
|
||||
ev, cnt->GetOriginalExpression(),
|
||||
cmStrCat(
|
||||
"sub-command SORT, an invalid COMPARE option has been "
|
||||
"specified: \"",
|
||||
option, "\"."));
|
||||
return std::string{};
|
||||
}
|
||||
if (cmHasPrefix(arg, CASE)) {
|
||||
if (sortConfig.Case !=
|
||||
SortConfig::CaseSensitivity::DEFAULT) {
|
||||
case SortOptionResult::NotRecognized:
|
||||
reportError(ev, cnt->GetOriginalExpression(),
|
||||
"sub-command SORT, CASE option has been "
|
||||
"specified multiple times.");
|
||||
cmStrCat("sub-command SORT, option \"", arg,
|
||||
"\" is invalid."));
|
||||
return std::string{};
|
||||
}
|
||||
auto option = cm::string_view{ arg.c_str() + CASE.length() };
|
||||
if (option == "SENSITIVE"_s) {
|
||||
sortConfig.Case = SortConfig::CaseSensitivity::SENSITIVE;
|
||||
continue;
|
||||
}
|
||||
if (option == "INSENSITIVE"_s) {
|
||||
sortConfig.Case = SortConfig::CaseSensitivity::INSENSITIVE;
|
||||
continue;
|
||||
}
|
||||
reportError(
|
||||
ev, cnt->GetOriginalExpression(),
|
||||
cmStrCat(
|
||||
"sub-command SORT, an invalid CASE option has been "
|
||||
"specified: \"",
|
||||
option, "\"."));
|
||||
return std::string{};
|
||||
}
|
||||
if (cmHasPrefix(arg, ORDER)) {
|
||||
if (sortConfig.Order != SortConfig::OrderMode::DEFAULT) {
|
||||
reportError(ev, cnt->GetOriginalExpression(),
|
||||
"sub-command SORT, ORDER option has been "
|
||||
"specified multiple times.");
|
||||
return std::string{};
|
||||
}
|
||||
auto option =
|
||||
cm::string_view{ arg.c_str() + ORDER.length() };
|
||||
if (option == "ASCENDING"_s) {
|
||||
sortConfig.Order = SortConfig::OrderMode::ASCENDING;
|
||||
continue;
|
||||
}
|
||||
if (option == "DESCENDING"_s) {
|
||||
sortConfig.Order = SortConfig::OrderMode::DESCENDING;
|
||||
continue;
|
||||
}
|
||||
reportError(
|
||||
ev, cnt->GetOriginalExpression(),
|
||||
cmStrCat(
|
||||
"sub-command SORT, an invalid ORDER option has been "
|
||||
"specified: \"",
|
||||
option, "\"."));
|
||||
return std::string{};
|
||||
}
|
||||
reportError(ev, cnt->GetOriginalExpression(),
|
||||
cmStrCat("sub-command SORT, option \"", arg,
|
||||
"\" is invalid."));
|
||||
return std::string{};
|
||||
}
|
||||
|
||||
return list.sort(sortConfig).to_string();
|
||||
@@ -6285,7 +6424,8 @@ cmGeneratorExpressionNode const* cmGeneratorExpressionNode::GetNode(
|
||||
{ "PATH_EQUAL", &pathEqualNode },
|
||||
{ "MAKE_C_IDENTIFIER", &makeCIdentifierNode },
|
||||
{ "BOOL", &boolNode },
|
||||
{ "_0", &boundOperandNode },
|
||||
{ "_0", &boundOperandNode0 },
|
||||
{ "_1", &boundOperandNode1 },
|
||||
{ "IF", &ifNode },
|
||||
{ "ANGLE-R", &angle_rNode },
|
||||
{ "COMMA", &commaNode },
|
||||
|
||||
+19
-5
@@ -382,7 +382,9 @@ cmList& cmList::sort(SortConfiguration cfg)
|
||||
return *this;
|
||||
}
|
||||
|
||||
cmList& cmList::sort(SortConfiguration cfg, cmMakefile& makefile)
|
||||
cmList& cmList::sort(
|
||||
SortConfiguration cfg,
|
||||
std::function<bool(std::string const&, std::string const&)> comparator)
|
||||
{
|
||||
SortConfiguration config{ cfg };
|
||||
|
||||
@@ -394,11 +396,10 @@ cmList& cmList::sort(SortConfiguration cfg, cmMakefile& makefile)
|
||||
}
|
||||
|
||||
try {
|
||||
ComparatorEvaluator evaluator(config.ComparatorFunction, makefile);
|
||||
StringSorter sorter(
|
||||
config, [&evaluator](std::string const& a, std::string const& b) {
|
||||
bool result = evaluator(a, b);
|
||||
if (result && evaluator(b, a)) {
|
||||
config, [&comparator](std::string const& a, std::string const& b) {
|
||||
bool result = comparator(a, b);
|
||||
if (result && comparator(b, a)) {
|
||||
throw cmList::transform_error(
|
||||
"sub-command SORT, COMPARATOR: function does not induce a strict "
|
||||
"weak ordering. The comparator returned TRUE for both (a, b) and "
|
||||
@@ -414,6 +415,19 @@ cmList& cmList::sort(SortConfiguration cfg, cmMakefile& makefile)
|
||||
return *this;
|
||||
}
|
||||
|
||||
cmList& cmList::sort(SortConfiguration cfg, cmMakefile& makefile)
|
||||
{
|
||||
try {
|
||||
ComparatorEvaluator evaluator(cfg.ComparatorFunction, makefile);
|
||||
return this->sort(
|
||||
cfg, [&evaluator](std::string const& a, std::string const& b) {
|
||||
return evaluator(a, b);
|
||||
});
|
||||
} catch (transform_error& e) {
|
||||
throw std::invalid_argument(e.what());
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
using transform_type = std::function<std::string(std::string const&)>;
|
||||
using transform_error = cmList::transform_error;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <functional>
|
||||
#include <initializer_list>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
@@ -873,6 +874,9 @@ public:
|
||||
};
|
||||
cmList& sort(SortConfiguration config = SortConfiguration{});
|
||||
cmList& sort(SortConfiguration config, cmMakefile& makefile);
|
||||
cmList& sort(
|
||||
SortConfiguration config,
|
||||
std::function<bool(std::string const&, std::string const&)> comparator);
|
||||
|
||||
// exception raised on error during transform operations
|
||||
class transform_error : public std::runtime_error
|
||||
|
||||
@@ -17,6 +17,7 @@ set(CMakeLib_TESTS
|
||||
testGenExBoundOperand.cxx
|
||||
testGenExTransformApply.cxx
|
||||
testGenExListPredicate.cxx
|
||||
testGenExListSortComparator.cxx
|
||||
testJSONHelpers.cxx
|
||||
testRST.cxx
|
||||
testRange.cxx
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
file LICENSE.rst or https://cmake.org/licensing for details. */
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "cmGenExContext.h"
|
||||
|
||||
@@ -9,22 +10,57 @@ static bool testContextBinding()
|
||||
{
|
||||
cm::GenEx::Context ctx(nullptr, "Debug");
|
||||
bool ok = true;
|
||||
if (ctx.HasBoundOperand()) {
|
||||
if (ctx.HasBoundOperand() || ctx.BoundOperandCount() != 0) {
|
||||
std::cerr << "binding should start unset\n";
|
||||
ok = false;
|
||||
}
|
||||
ctx.SetBoundOperand("net");
|
||||
if (!ctx.HasBoundOperand() || ctx.GetBoundOperand() != "net") {
|
||||
if (!ctx.HasBoundOperand() || ctx.BoundOperandCount() != 1 ||
|
||||
ctx.GetBoundOperand() != "net") {
|
||||
std::cerr << "binding did not round-trip\n";
|
||||
ok = false;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
static bool testContextMultipleOperands()
|
||||
{
|
||||
cm::GenEx::Context ctx(nullptr, "Debug");
|
||||
bool ok = true;
|
||||
ctx.SetBoundOperands({ "a", "b" });
|
||||
if (ctx.BoundOperandCount() != 2 || !ctx.HasBoundOperand(0) ||
|
||||
!ctx.HasBoundOperand(1) || ctx.GetBoundOperand(0) != "a" ||
|
||||
ctx.GetBoundOperand(1) != "b") {
|
||||
std::cerr << "two-operand binding did not round-trip\n";
|
||||
ok = false;
|
||||
}
|
||||
if (ctx.HasBoundOperand(2)) {
|
||||
std::cerr << "index past the frame should be out of range\n";
|
||||
ok = false;
|
||||
}
|
||||
// Re-binding replaces the whole frame, which the shadow/restore of nested
|
||||
// bindings relies on.
|
||||
ctx.SetBoundOperand("x");
|
||||
if (ctx.BoundOperandCount() != 1 || ctx.HasBoundOperand(1) ||
|
||||
ctx.GetBoundOperand(0) != "x") {
|
||||
std::cerr << "re-binding did not replace the frame\n";
|
||||
ok = false;
|
||||
}
|
||||
ctx.SetBoundOperands({});
|
||||
if (ctx.BoundOperandCount() != 0 || ctx.HasBoundOperand(0)) {
|
||||
std::cerr << "empty frame should clear the binding\n";
|
||||
ok = false;
|
||||
}
|
||||
return ok;
|
||||
}
|
||||
|
||||
int testGenExBoundOperand(int /*argc*/, char* /*argv*/[])
|
||||
{
|
||||
if (!testContextBinding()) {
|
||||
return 1;
|
||||
}
|
||||
if (!testContextMultipleOperands()) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
/* 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");
|
||||
}
|
||||
};
|
||||
|
||||
bool expectEq(char const* name, std::string const& got,
|
||||
std::string const& want)
|
||||
{
|
||||
if (got != want) {
|
||||
std::cerr << name << ": expected '" << want << "', got '" << got << "'\n";
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static bool testSortNumericAscending()
|
||||
{
|
||||
GenExFixture fx;
|
||||
return expectEq(
|
||||
"testSortNumericAscending",
|
||||
fx.Eval("$<LIST:SORT,3;1;2,COMPARATOR,$<STRLESS:$<_0>,$<_1>>>"), "1;2;3");
|
||||
}
|
||||
|
||||
static bool testSortByExtension()
|
||||
{
|
||||
GenExFixture fx;
|
||||
return expectEq("testSortByExtension",
|
||||
fx.Eval("$<LIST:SORT,c.z;a.a;b.m,COMPARATOR,"
|
||||
"$<STRLESS:$<PATH:GET_EXTENSION,$<_0>>,"
|
||||
"$<PATH:GET_EXTENSION,$<_1>>>>"),
|
||||
"a.a;b.m;c.z");
|
||||
}
|
||||
|
||||
static bool testSortDescending()
|
||||
{
|
||||
GenExFixture fx;
|
||||
return expectEq(
|
||||
"testSortDescending",
|
||||
fx.Eval(
|
||||
"$<LIST:SORT,3;1;2,COMPARATOR,$<STRLESS:$<_0>,$<_1>>,ORDER:DESCENDING>"),
|
||||
"3;2;1");
|
||||
}
|
||||
|
||||
static bool testSortNestedBinding()
|
||||
{
|
||||
GenExFixture fx;
|
||||
// Nested binding: the inner APPLY rebinds $<_0> but the outer $<_1> is
|
||||
// restored after it, so this reduces to STRLESS(a, b) ascending.
|
||||
return expectEq(
|
||||
"testSortNestedBinding",
|
||||
fx.Eval("$<LIST:SORT,y;x,COMPARATOR,"
|
||||
"$<STRLESS:$<LIST:TRANSFORM,$<_0>,APPLY,$<_0>>,$<_1>>>"),
|
||||
"x;y");
|
||||
}
|
||||
|
||||
static bool testSortEmpty()
|
||||
{
|
||||
GenExFixture fx;
|
||||
return expectEq("testSortEmpty",
|
||||
fx.Eval("$<LIST:SORT,,COMPARATOR,$<STRLESS:$<_0>,$<_1>>>"),
|
||||
"");
|
||||
}
|
||||
|
||||
static bool testSortSingle()
|
||||
{
|
||||
GenExFixture fx;
|
||||
return expectEq("testSortSingle",
|
||||
fx.Eval("$<LIST:SORT,x,COMPARATOR,$<STRLESS:$<_0>,$<_1>>>"),
|
||||
"x");
|
||||
}
|
||||
|
||||
static bool testSortEqualElements()
|
||||
{
|
||||
GenExFixture fx;
|
||||
// Equal elements are FALSE both ways, so the strict-weak-ordering guard must
|
||||
// not trip and the duplicates are preserved.
|
||||
return expectEq(
|
||||
"testSortEqualElements",
|
||||
fx.Eval("$<LIST:SORT,b;a;b,COMPARATOR,$<STRLESS:$<_0>,$<_1>>>"), "a;b;b");
|
||||
}
|
||||
|
||||
static bool testSortCaseInsensitive()
|
||||
{
|
||||
// CASE:INSENSITIVE case-folds the body's operands, so B;a;C orders as a;B;C
|
||||
// (elements keep their original case).
|
||||
GenExFixture fx;
|
||||
return expectEq(
|
||||
"testSortCaseInsensitive",
|
||||
fx.Eval(
|
||||
"$<LIST:SORT,B;a;C,COMPARATOR,$<STRLESS:$<_0>,$<_1>>,CASE:INSENSITIVE>"),
|
||||
"a;B;C");
|
||||
}
|
||||
|
||||
int testGenExListSortComparator(int /*argc*/, char* /*argv*/[])
|
||||
{
|
||||
if (!testSortNumericAscending()) {
|
||||
return 1;
|
||||
}
|
||||
if (!testSortByExtension()) {
|
||||
return 1;
|
||||
}
|
||||
if (!testSortDescending()) {
|
||||
return 1;
|
||||
}
|
||||
if (!testSortNestedBinding()) {
|
||||
return 1;
|
||||
}
|
||||
if (!testSortEmpty()) {
|
||||
return 1;
|
||||
}
|
||||
if (!testSortSingle()) {
|
||||
return 1;
|
||||
}
|
||||
if (!testSortEqualElements()) {
|
||||
return 1;
|
||||
}
|
||||
if (!testSortCaseInsensitive()) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1 @@
|
||||
is out of range for the current binding operation
|
||||
@@ -0,0 +1,4 @@
|
||||
# $<_1> requires a binary binding (e.g. SORT COMPARATOR); using it in a unary
|
||||
# APPLY body, which binds only $<_0>, is an error.
|
||||
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/x.txt"
|
||||
CONTENT "$<LIST:TRANSFORM,a;b,APPLY,X$<_1>Y>")
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1 @@
|
||||
option "COMPARE" is incompatible with "COMPARATOR"
|
||||
@@ -0,0 +1,2 @@
|
||||
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/x.txt"
|
||||
CONTENT "$<LIST:SORT,a;b,COMPARATOR,$<STRLESS:$<_0>,$<_1>>,COMPARE:STRING>")
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1 @@
|
||||
option "BOGUS:X" is invalid
|
||||
@@ -0,0 +1,4 @@
|
||||
# An unrecognized trailing option on the COMPARATOR path goes through the
|
||||
# NotRecognized branch of the shared option parser.
|
||||
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/x.txt"
|
||||
CONTENT "$<LIST:SORT,a;b,COMPARATOR,$<STRLESS:$<_0>,$<_1>>,BOGUS:X>")
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1 @@
|
||||
sub-command SORT, COMPARATOR expects a <body> argument
|
||||
@@ -0,0 +1,2 @@
|
||||
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/x.txt"
|
||||
CONTENT "$<LIST:SORT,a;b,COMPARATOR>")
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1 @@
|
||||
COMPARATOR body must evaluate to "0" or "1"
|
||||
@@ -0,0 +1,2 @@
|
||||
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/x.txt"
|
||||
CONTENT "$<LIST:SORT,a;b,COMPARATOR,maybe>")
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1 @@
|
||||
does not induce a strict weak
|
||||
@@ -0,0 +1,2 @@
|
||||
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/x.txt"
|
||||
CONTENT "$<LIST:SORT,a;b;c,COMPARATOR,1>")
|
||||
@@ -0,0 +1,5 @@
|
||||
file(READ "${RunCMake_TEST_BINARY_DIR}/out.txt" actual)
|
||||
string(STRIP "${actual}" actual)
|
||||
if(NOT actual STREQUAL "b;c;a")
|
||||
set(RunCMake_TEST_FAILED "unexpected output: [${actual}]")
|
||||
endif()
|
||||
@@ -0,0 +1,10 @@
|
||||
add_custom_target(a)
|
||||
add_custom_target(b)
|
||||
add_custom_target(c)
|
||||
set_property(TARGET a PROPERTY MY_RANK 3)
|
||||
set_property(TARGET b PROPERTY MY_RANK 1)
|
||||
set_property(TARGET c PROPERTY MY_RANK 2)
|
||||
|
||||
# Sort the target names by their MY_RANK property, ascending.
|
||||
file(GENERATE OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/out.txt"
|
||||
CONTENT "$<LIST:SORT,a;b;c,COMPARATOR,$<STRLESS:$<TARGET_PROPERTY:$<_0>,MY_RANK>,$<TARGET_PROPERTY:$<_1>,MY_RANK>>>\n")
|
||||
@@ -72,6 +72,13 @@ run_cmake(ListTransformPredicateLinkLibraries)
|
||||
run_cmake(ListFilterPredicateMissingBody)
|
||||
run_cmake(ListFilterPredicateNonBool)
|
||||
run_cmake(BoundOperandOutsideBinding)
|
||||
run_cmake(BoundOperand1OutsideBinding)
|
||||
run_cmake(ListSortComparatorNonBool)
|
||||
run_cmake(ListSortComparatorNotStrictWeak)
|
||||
run_cmake(ListSortComparatorCompareConflict)
|
||||
run_cmake(ListSortComparatorInvalidOption)
|
||||
run_cmake(ListSortComparatorMissingBody)
|
||||
run_cmake(ListSortComparatorTargetProperty)
|
||||
|
||||
function(run_cmake_build test)
|
||||
set(RunCMake_TEST_BINARY_DIR ${RunCMake_BINARY_DIR}/${test}-build)
|
||||
|
||||
Reference in New Issue
Block a user