2022-01-26 19:02:20 +01:00
|
|
|
/*
|
|
|
|
* Cppcheck - A tool for static C/C++ code analysis
|
2023-01-28 10:16:34 +01:00
|
|
|
* Copyright (C) 2007-2023 Cppcheck team.
|
2022-01-26 19:02:20 +01:00
|
|
|
*
|
|
|
|
* This program is free software: you can redistribute it and/or modify
|
|
|
|
* it under the terms of the GNU General Public License as published by
|
|
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
|
|
* (at your option) any later version.
|
|
|
|
*
|
|
|
|
* This program is distributed in the hope that it will be useful,
|
|
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
* GNU General Public License for more details.
|
|
|
|
*
|
|
|
|
* You should have received a copy of the GNU General Public License
|
|
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
|
|
*/
|
|
|
|
|
2020-11-10 16:00:55 +01:00
|
|
|
#include "reverseanalyzer.h"
|
2022-01-27 19:03:20 +01:00
|
|
|
|
2020-11-10 16:00:55 +01:00
|
|
|
#include "analyzer.h"
|
|
|
|
#include "astutils.h"
|
2021-03-20 14:02:07 +01:00
|
|
|
#include "errortypes.h"
|
2020-11-10 16:00:55 +01:00
|
|
|
#include "forwardanalyzer.h"
|
2022-01-27 19:03:20 +01:00
|
|
|
#include "mathlib.h"
|
2021-07-18 07:46:31 +02:00
|
|
|
#include "settings.h"
|
2020-11-10 16:00:55 +01:00
|
|
|
#include "symboldatabase.h"
|
|
|
|
#include "token.h"
|
|
|
|
#include "valueptr.h"
|
|
|
|
|
|
|
|
#include <algorithm>
|
2022-01-27 19:03:20 +01:00
|
|
|
#include <cstddef>
|
|
|
|
#include <string>
|
|
|
|
#include <tuple>
|
|
|
|
#include <utility>
|
|
|
|
#include <vector>
|
2020-11-10 16:00:55 +01:00
|
|
|
|
|
|
|
struct ReverseTraversal {
|
2023-03-07 12:26:17 +01:00
|
|
|
ReverseTraversal(const ValuePtr<Analyzer>& analyzer, const Settings& settings)
|
2020-11-10 16:00:55 +01:00
|
|
|
: analyzer(analyzer), settings(settings)
|
|
|
|
{}
|
|
|
|
ValuePtr<Analyzer> analyzer;
|
2023-03-07 12:26:17 +01:00
|
|
|
const Settings& settings;
|
2020-11-10 16:00:55 +01:00
|
|
|
|
2023-03-02 21:51:58 +01:00
|
|
|
std::pair<bool, bool> evalCond(const Token* tok) const {
|
2021-10-11 19:10:37 +02:00
|
|
|
std::vector<MathLib::bigint> result = analyzer->evaluate(tok);
|
2020-11-10 16:00:55 +01:00
|
|
|
// TODO: We should convert to bool
|
2022-12-30 15:13:47 +01:00
|
|
|
const bool checkThen = std::any_of(result.cbegin(), result.cend(), [](int x) {
|
2020-11-11 09:15:36 +01:00
|
|
|
return x == 1;
|
|
|
|
});
|
2022-12-30 15:13:47 +01:00
|
|
|
const bool checkElse = std::any_of(result.cbegin(), result.cend(), [](int x) {
|
2020-11-11 09:15:36 +01:00
|
|
|
return x == 0;
|
|
|
|
});
|
2020-11-10 16:00:55 +01:00
|
|
|
return std::make_pair(checkThen, checkElse);
|
|
|
|
}
|
|
|
|
|
2020-11-11 09:15:36 +01:00
|
|
|
bool update(Token* tok) {
|
2020-11-10 16:00:55 +01:00
|
|
|
Analyzer::Action action = analyzer->analyze(tok, Analyzer::Direction::Reverse);
|
|
|
|
if (action.isInconclusive() && !analyzer->lowerToInconclusive())
|
|
|
|
return false;
|
|
|
|
if (action.isInvalid())
|
|
|
|
return false;
|
2023-02-23 18:04:16 +01:00
|
|
|
if (!action.isNone())
|
|
|
|
analyzer->update(tok, action, Analyzer::Direction::Reverse);
|
2020-11-10 16:00:55 +01:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
2022-05-11 20:01:22 +02:00
|
|
|
static Token* getParentFunction(Token* tok)
|
2021-11-25 22:34:00 +01:00
|
|
|
{
|
|
|
|
if (!tok)
|
|
|
|
return nullptr;
|
|
|
|
if (!tok->astParent())
|
|
|
|
return nullptr;
|
|
|
|
int argn = -1;
|
|
|
|
if (Token* ftok = getTokenArgumentFunction(tok, argn)) {
|
|
|
|
while (!Token::Match(ftok, "(|{")) {
|
|
|
|
if (!ftok)
|
|
|
|
return nullptr;
|
|
|
|
if (ftok->index() >= tok->index())
|
|
|
|
return nullptr;
|
2023-03-30 07:22:41 +02:00
|
|
|
if (!ftok->link() || ftok->str() == ")")
|
2021-11-25 22:34:00 +01:00
|
|
|
ftok = ftok->next();
|
2023-03-30 07:22:41 +02:00
|
|
|
else
|
|
|
|
ftok = ftok->link()->next();
|
2021-11-25 22:34:00 +01:00
|
|
|
}
|
|
|
|
if (ftok == tok)
|
|
|
|
return nullptr;
|
|
|
|
return ftok;
|
|
|
|
}
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2022-05-11 20:01:22 +02:00
|
|
|
static Token* getTopFunction(Token* tok)
|
2021-11-25 22:34:00 +01:00
|
|
|
{
|
|
|
|
if (!tok)
|
|
|
|
return nullptr;
|
|
|
|
if (!tok->astParent())
|
|
|
|
return tok;
|
|
|
|
Token* parent = tok;
|
|
|
|
Token* top = tok;
|
|
|
|
while ((parent = getParentFunction(parent)))
|
|
|
|
top = parent;
|
|
|
|
return top;
|
|
|
|
}
|
|
|
|
|
2020-11-11 09:15:36 +01:00
|
|
|
bool updateRecursive(Token* start) {
|
2020-11-10 16:00:55 +01:00
|
|
|
bool continueB = true;
|
|
|
|
visitAstNodes(start, [&](Token* tok) {
|
2021-06-04 17:20:21 +02:00
|
|
|
const Token* parent = tok->astParent();
|
|
|
|
while (Token::simpleMatch(parent, ":"))
|
|
|
|
parent = parent->astParent();
|
|
|
|
if (isUnevaluated(tok) || isDeadCode(tok, parent))
|
|
|
|
return ChildrenToVisit::none;
|
2020-11-10 16:00:55 +01:00
|
|
|
continueB &= update(tok);
|
|
|
|
if (continueB)
|
|
|
|
return ChildrenToVisit::op1_and_op2;
|
2023-06-20 18:43:21 +02:00
|
|
|
return ChildrenToVisit::done;
|
2020-11-10 16:00:55 +01:00
|
|
|
});
|
|
|
|
return continueB;
|
|
|
|
}
|
|
|
|
|
2023-05-28 01:11:59 +02:00
|
|
|
Analyzer::Action analyzeRecursive(const Token* start) const {
|
2020-11-10 16:00:55 +01:00
|
|
|
Analyzer::Action result = Analyzer::Action::None;
|
|
|
|
visitAstNodes(start, [&](const Token* tok) {
|
|
|
|
result |= analyzer->analyze(tok, Analyzer::Direction::Reverse);
|
|
|
|
if (result.isModified())
|
|
|
|
return ChildrenToVisit::done;
|
|
|
|
return ChildrenToVisit::op1_and_op2;
|
|
|
|
});
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2023-03-03 11:02:51 +01:00
|
|
|
Analyzer::Action analyzeRange(const Token* start, const Token* end) const {
|
2020-11-10 16:00:55 +01:00
|
|
|
Analyzer::Action result = Analyzer::Action::None;
|
|
|
|
for (const Token* tok = start; tok && tok != end; tok = tok->next()) {
|
|
|
|
Analyzer::Action action = analyzer->analyze(tok, Analyzer::Direction::Reverse);
|
|
|
|
if (action.isModified())
|
|
|
|
return action;
|
|
|
|
result |= action;
|
|
|
|
}
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
2023-03-02 21:51:58 +01:00
|
|
|
Token* isDeadCode(Token* tok, const Token* end = nullptr) const {
|
2020-11-10 16:00:55 +01:00
|
|
|
int opSide = 0;
|
|
|
|
for (; tok && tok->astParent(); tok = tok->astParent()) {
|
2021-06-04 17:20:21 +02:00
|
|
|
if (tok == end)
|
|
|
|
break;
|
2020-11-10 16:00:55 +01:00
|
|
|
Token* parent = tok->astParent();
|
|
|
|
if (Token::simpleMatch(parent, ":")) {
|
|
|
|
if (astIsLHS(tok))
|
|
|
|
opSide = 1;
|
|
|
|
else if (astIsRHS(tok))
|
|
|
|
opSide = 2;
|
|
|
|
else
|
|
|
|
opSide = 0;
|
|
|
|
}
|
2021-02-17 12:09:11 +01:00
|
|
|
if (tok != parent->astOperand2())
|
|
|
|
continue;
|
2022-01-04 21:19:45 +01:00
|
|
|
if (Token::simpleMatch(parent, ":"))
|
|
|
|
parent = parent->astParent();
|
2020-11-10 16:00:55 +01:00
|
|
|
if (!Token::Match(parent, "%oror%|&&|?"))
|
|
|
|
continue;
|
2023-05-22 19:53:51 +02:00
|
|
|
const Token* condTok = parent->astOperand1();
|
2020-11-10 16:00:55 +01:00
|
|
|
if (!condTok)
|
|
|
|
continue;
|
|
|
|
bool checkThen, checkElse;
|
|
|
|
std::tie(checkThen, checkElse) = evalCond(condTok);
|
|
|
|
|
|
|
|
if (parent->str() == "?") {
|
2021-02-17 12:09:11 +01:00
|
|
|
if (checkElse && opSide == 1)
|
2020-11-10 16:00:55 +01:00
|
|
|
return parent;
|
2021-02-17 12:09:11 +01:00
|
|
|
if (checkThen && opSide == 2)
|
2020-11-10 16:00:55 +01:00
|
|
|
return parent;
|
|
|
|
}
|
|
|
|
if (!checkThen && parent->str() == "&&")
|
|
|
|
return parent;
|
|
|
|
if (!checkElse && parent->str() == "||")
|
|
|
|
return parent;
|
|
|
|
}
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2021-01-25 22:51:50 +01:00
|
|
|
void traverse(Token* start, const Token* end = nullptr) {
|
2021-01-23 17:52:01 +01:00
|
|
|
if (start == end)
|
|
|
|
return;
|
2021-03-20 14:02:07 +01:00
|
|
|
std::size_t i = start->index();
|
2022-01-13 17:24:26 +01:00
|
|
|
for (Token* tok = start->previous(); succeeds(tok, end); tok = tok->previous()) {
|
2021-03-20 14:02:07 +01:00
|
|
|
if (tok->index() >= i)
|
|
|
|
throw InternalError(tok, "Cyclic reverse analysis.");
|
|
|
|
i = tok->index();
|
2020-11-10 16:00:55 +01:00
|
|
|
if (tok == start || (tok->str() == "{" && (tok->scope()->type == Scope::ScopeType::eFunction ||
|
2021-08-07 20:51:18 +02:00
|
|
|
tok->scope()->type == Scope::ScopeType::eLambda))) {
|
2021-11-11 08:01:10 +01:00
|
|
|
const Function* f = tok->scope()->function;
|
|
|
|
if (f && f->isConstructor()) {
|
|
|
|
if (const Token* initList = f->constructorMemberInitialization())
|
|
|
|
traverse(tok->previous(), tok->tokAt(initList->index() - tok->index()));
|
|
|
|
}
|
2020-11-10 16:00:55 +01:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
if (Token::Match(tok, "return|break|continue"))
|
|
|
|
break;
|
2020-12-14 11:13:47 +01:00
|
|
|
if (Token::Match(tok, "%name% :"))
|
|
|
|
break;
|
2021-02-17 12:09:11 +01:00
|
|
|
if (Token::simpleMatch(tok, ":"))
|
|
|
|
continue;
|
2020-11-10 16:00:55 +01:00
|
|
|
// Evaluate LHS of assignment before RHS
|
|
|
|
if (Token* assignTok = assignExpr(tok)) {
|
2020-12-26 21:26:39 +01:00
|
|
|
// If assignTok has broken ast then stop
|
|
|
|
if (!assignTok->astOperand1() || !assignTok->astOperand2())
|
|
|
|
break;
|
2020-11-10 16:00:55 +01:00
|
|
|
Token* assignTop = assignTok;
|
|
|
|
bool continueB = true;
|
|
|
|
while (assignTop->isAssignmentOp()) {
|
|
|
|
if (!Token::Match(assignTop->astOperand1(), "%assign%")) {
|
|
|
|
continueB &= updateRecursive(assignTop->astOperand1());
|
|
|
|
}
|
|
|
|
if (!assignTop->astParent())
|
|
|
|
break;
|
|
|
|
assignTop = assignTop->astParent();
|
|
|
|
}
|
|
|
|
// Is assignment in dead code
|
|
|
|
if (Token* parent = isDeadCode(assignTok)) {
|
|
|
|
tok = parent;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
// Simple assign
|
|
|
|
if (assignTok->astParent() == assignTop || assignTok == assignTop) {
|
|
|
|
Analyzer::Action rhsAction =
|
|
|
|
analyzer->analyze(assignTok->astOperand2(), Analyzer::Direction::Reverse);
|
|
|
|
Analyzer::Action lhsAction =
|
|
|
|
analyzer->analyze(assignTok->astOperand1(), Analyzer::Direction::Reverse);
|
|
|
|
// Assignment from
|
2021-08-03 06:31:52 +02:00
|
|
|
if (rhsAction.isRead() && !lhsAction.isInvalid() && assignTok->astOperand1()->exprId() > 0) {
|
2020-11-10 16:00:55 +01:00
|
|
|
const std::string info = "Assignment from '" + assignTok->expressionString() + "'";
|
|
|
|
ValuePtr<Analyzer> a = analyzer->reanalyze(assignTok->astOperand1(), info);
|
|
|
|
if (a) {
|
|
|
|
valueFlowGenericForward(nextAfterAstRightmostLeaf(assignTok->astOperand2()),
|
|
|
|
assignTok->astOperand2()->scope()->bodyEnd,
|
|
|
|
a,
|
|
|
|
settings);
|
|
|
|
}
|
|
|
|
// Assignment to
|
2021-07-30 21:29:35 +02:00
|
|
|
} else if (lhsAction.matches() && !assignTok->astOperand2()->hasKnownIntValue() &&
|
2021-08-03 06:31:52 +02:00
|
|
|
assignTok->astOperand2()->exprId() > 0 &&
|
2023-03-07 12:26:17 +01:00
|
|
|
isConstExpression(assignTok->astOperand2(), settings.library, true)) {
|
2020-11-10 16:00:55 +01:00
|
|
|
const std::string info = "Assignment to '" + assignTok->expressionString() + "'";
|
|
|
|
ValuePtr<Analyzer> a = analyzer->reanalyze(assignTok->astOperand2(), info);
|
|
|
|
if (a) {
|
|
|
|
valueFlowGenericForward(nextAfterAstRightmostLeaf(assignTok->astOperand2()),
|
|
|
|
assignTok->astOperand2()->scope()->bodyEnd,
|
|
|
|
a,
|
|
|
|
settings);
|
2021-01-23 17:52:01 +01:00
|
|
|
valueFlowGenericReverse(assignTok->astOperand1()->previous(), end, a, settings);
|
2020-11-10 16:00:55 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!continueB)
|
|
|
|
break;
|
2021-06-04 17:20:21 +02:00
|
|
|
if (!updateRecursive(assignTop->astOperand2()))
|
2020-12-27 10:14:46 +01:00
|
|
|
break;
|
2020-11-20 09:36:09 +01:00
|
|
|
tok = previousBeforeAstLeftmostLeaf(assignTop)->next();
|
2020-11-10 16:00:55 +01:00
|
|
|
continue;
|
|
|
|
}
|
2021-11-25 22:34:00 +01:00
|
|
|
if (tok->str() == ")" && !isUnevaluated(tok)) {
|
|
|
|
if (Token* top = getTopFunction(tok->link())) {
|
|
|
|
if (!updateRecursive(top))
|
|
|
|
break;
|
|
|
|
Token* next = previousBeforeAstLeftmostLeaf(top);
|
|
|
|
if (next && precedes(next, tok))
|
|
|
|
tok = next->next();
|
|
|
|
}
|
|
|
|
continue;
|
|
|
|
}
|
2020-11-10 16:00:55 +01:00
|
|
|
if (tok->str() == "}") {
|
|
|
|
Token* condTok = getCondTokFromEnd(tok);
|
|
|
|
if (!condTok)
|
|
|
|
break;
|
|
|
|
Analyzer::Action condAction = analyzeRecursive(condTok);
|
|
|
|
const bool inLoop = condTok->astTop() && Token::Match(condTok->astTop()->previous(), "for|while (");
|
|
|
|
// Evaluate condition of for and while loops first
|
|
|
|
if (inLoop) {
|
2022-12-08 07:27:06 +01:00
|
|
|
if (Token::findmatch(tok->link(), "goto|break", tok))
|
|
|
|
break;
|
2020-11-10 16:00:55 +01:00
|
|
|
if (condAction.isModified())
|
|
|
|
break;
|
|
|
|
valueFlowGenericForward(condTok, analyzer, settings);
|
|
|
|
}
|
2020-11-18 15:43:09 +01:00
|
|
|
Token* thenEnd;
|
2020-11-10 16:00:55 +01:00
|
|
|
const bool hasElse = Token::simpleMatch(tok->link()->tokAt(-2), "} else {");
|
|
|
|
if (hasElse) {
|
|
|
|
thenEnd = tok->link()->tokAt(-2);
|
|
|
|
} else {
|
|
|
|
thenEnd = tok;
|
|
|
|
}
|
|
|
|
|
|
|
|
Analyzer::Action thenAction = analyzeRange(thenEnd->link(), thenEnd);
|
|
|
|
Analyzer::Action elseAction = Analyzer::Action::None;
|
|
|
|
if (hasElse) {
|
|
|
|
elseAction = analyzeRange(tok->link(), tok);
|
|
|
|
}
|
|
|
|
if (thenAction.isModified() && inLoop)
|
|
|
|
break;
|
2023-06-20 18:43:21 +02:00
|
|
|
if (thenAction.isModified() && !elseAction.isModified())
|
2021-06-09 09:20:43 +02:00
|
|
|
analyzer->assume(condTok, hasElse);
|
2020-11-10 16:00:55 +01:00
|
|
|
else if (elseAction.isModified() && !thenAction.isModified())
|
2021-06-09 09:20:43 +02:00
|
|
|
analyzer->assume(condTok, !hasElse);
|
2020-11-10 16:00:55 +01:00
|
|
|
// Bail if one of the branches are read to avoid FPs due to over constraints
|
|
|
|
else if (thenAction.isIdempotent() || elseAction.isIdempotent() || thenAction.isRead() ||
|
|
|
|
elseAction.isRead())
|
|
|
|
break;
|
|
|
|
if (thenAction.isInvalid() || elseAction.isInvalid())
|
|
|
|
break;
|
|
|
|
|
|
|
|
if (!thenAction.isModified() && !elseAction.isModified())
|
|
|
|
valueFlowGenericForward(condTok, analyzer, settings);
|
|
|
|
else if (condAction.isRead())
|
|
|
|
break;
|
|
|
|
// If the condition modifies the variable then bail
|
|
|
|
if (condAction.isModified())
|
|
|
|
break;
|
|
|
|
tok = condTok->astTop()->previous();
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (tok->str() == "{") {
|
|
|
|
if (tok->previous() &&
|
|
|
|
(Token::simpleMatch(tok->previous(), "do") ||
|
|
|
|
(tok->strAt(-1) == ")" && Token::Match(tok->linkAt(-1)->previous(), "for|while (")))) {
|
|
|
|
Analyzer::Action action = analyzeRange(tok, tok->link());
|
|
|
|
if (action.isModified())
|
|
|
|
break;
|
|
|
|
}
|
2021-12-06 20:06:48 +01:00
|
|
|
Token* condTok = getCondTokFromEnd(tok->link());
|
|
|
|
if (condTok) {
|
|
|
|
Analyzer::Result r = valueFlowGenericForward(condTok, analyzer, settings);
|
|
|
|
if (r.action.isModified())
|
|
|
|
break;
|
|
|
|
}
|
2020-11-10 16:00:55 +01:00
|
|
|
if (Token::simpleMatch(tok->tokAt(-2), "} else {"))
|
|
|
|
tok = tok->linkAt(-2);
|
|
|
|
if (Token::simpleMatch(tok->previous(), ") {"))
|
|
|
|
tok = tok->previous()->link();
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (Token* next = isUnevaluated(tok)) {
|
|
|
|
tok = next;
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
if (Token* parent = isDeadCode(tok)) {
|
|
|
|
tok = parent;
|
|
|
|
continue;
|
|
|
|
}
|
2021-11-07 18:19:56 +01:00
|
|
|
if (tok->str() == "case") {
|
|
|
|
const Scope* scope = tok->scope();
|
|
|
|
while (scope && scope->type != Scope::eSwitch)
|
|
|
|
scope = scope->nestedIn;
|
|
|
|
if (!scope || scope->type != Scope::eSwitch)
|
|
|
|
break;
|
|
|
|
tok = tok->tokAt(scope->bodyStart->index() - tok->index() - 1);
|
|
|
|
continue;
|
|
|
|
}
|
2020-11-10 16:00:55 +01:00
|
|
|
if (!update(tok))
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-11-11 09:15:36 +01:00
|
|
|
static Token* assignExpr(Token* tok) {
|
2021-11-25 22:34:00 +01:00
|
|
|
if (Token::Match(tok, ")|}"))
|
|
|
|
tok = tok->link();
|
2020-11-10 16:00:55 +01:00
|
|
|
while (tok->astParent() && (astIsRHS(tok) || !tok->astParent()->isBinaryOp())) {
|
|
|
|
if (tok->astParent()->isAssignmentOp())
|
|
|
|
return tok->astParent();
|
|
|
|
tok = tok->astParent();
|
|
|
|
}
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
2020-11-11 09:15:36 +01:00
|
|
|
static Token* isUnevaluated(Token* tok) {
|
2020-11-10 16:00:55 +01:00
|
|
|
if (Token::Match(tok, ")|>") && tok->link()) {
|
|
|
|
Token* start = tok->link();
|
|
|
|
if (Token::Match(start->previous(), "sizeof|decltype ("))
|
|
|
|
return start->previous();
|
|
|
|
if (Token::simpleMatch(start, "<"))
|
|
|
|
return start;
|
|
|
|
}
|
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2023-03-07 12:26:17 +01:00
|
|
|
void valueFlowGenericReverse(Token* start, const Token* end, const ValuePtr<Analyzer>& a, const Settings& settings)
|
2021-01-23 17:52:01 +01:00
|
|
|
{
|
2023-06-07 11:11:48 +02:00
|
|
|
if (a->invalid())
|
|
|
|
return;
|
2021-01-23 17:52:01 +01:00
|
|
|
ReverseTraversal rt{a, settings};
|
|
|
|
rt.traverse(start, end);
|
|
|
|
}
|