summaryrefslogtreecommitdiffstats
path: root/compilerplugins/clang/flatten.cxx
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--compilerplugins/clang/flatten.cxx682
1 files changed, 682 insertions, 0 deletions
diff --git a/compilerplugins/clang/flatten.cxx b/compilerplugins/clang/flatten.cxx
new file mode 100644
index 000000000..bac4ae8a4
--- /dev/null
+++ b/compilerplugins/clang/flatten.cxx
@@ -0,0 +1,682 @@
+/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
+/*
+ * This file is part of the LibreOffice project.
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+#include "plugin.hxx"
+#include <cassert>
+#include <string>
+#include <iostream>
+#include <fstream>
+#include <set>
+#include <stack>
+
+/**
+ Look for places where we can flatten the control flow in a method by returning early.
+ */
+namespace {
+
+class Flatten:
+ public loplugin::FilteringRewritePlugin<Flatten>
+{
+public:
+ explicit Flatten(loplugin::InstantiationData const & data):
+ FilteringRewritePlugin(data) {}
+
+ virtual void run() override
+ {
+ TraverseDecl(compiler.getASTContext().getTranslationUnitDecl());
+ }
+
+ bool TraverseIfStmt(IfStmt *);
+ bool TraverseCXXCatchStmt(CXXCatchStmt * );
+ bool TraverseCompoundStmt(CompoundStmt *);
+ bool TraverseFunctionDecl(FunctionDecl *);
+ bool TraverseCXXMethodDecl(CXXMethodDecl *);
+ bool TraverseCXXConstructorDecl(CXXConstructorDecl *);
+ bool TraverseCXXConversionDecl(CXXConversionDecl *);
+ bool TraverseCXXDestructorDecl(CXXDestructorDecl *);
+ bool VisitIfStmt(IfStmt const * );
+private:
+ bool rewrite1(IfStmt const * );
+ bool rewrite2(IfStmt const * );
+ bool rewriteLargeIf(IfStmt const * );
+ SourceRange ignoreMacroExpansions(SourceRange range);
+ SourceRange extendOverComments(SourceRange range);
+ std::string getSourceAsString(SourceRange range);
+ llvm::Optional<std::string> invertCondition(Expr const * condExpr, SourceRange conditionRange);
+ bool isLargeCompoundStmt(Stmt const *);
+
+ Stmt const * lastStmtInCompoundStmt = nullptr;
+ FunctionDecl const * functionDecl = nullptr;
+ CompoundStmt const * functionDeclBody = nullptr;
+ Stmt const * mElseBranch = nullptr;
+};
+
+static Stmt const * containsSingleThrowExpr(Stmt const * stmt)
+{
+ if (auto compoundStmt = dyn_cast<CompoundStmt>(stmt)) {
+ if (compoundStmt->size() != 1)
+ return nullptr;
+ stmt = *compoundStmt->body_begin();
+ }
+ if (auto exprWithCleanups = dyn_cast<ExprWithCleanups>(stmt)) {
+ stmt = exprWithCleanups->getSubExpr();
+ }
+ return dyn_cast<CXXThrowExpr>(stmt);
+}
+
+static bool containsVarDecl(Stmt const * stmt)
+{
+ if (auto compoundStmt = dyn_cast<CompoundStmt>(stmt)) {
+ for (auto i = compoundStmt->body_begin(); i != compoundStmt->body_end(); ++i) {
+ auto declStmt = dyn_cast<DeclStmt>(*i);
+ if (declStmt && isa<VarDecl>(*declStmt->decl_begin()))
+ return true;
+ }
+ return false;
+ }
+ auto declStmt = dyn_cast<DeclStmt>(stmt);
+ return declStmt && isa<VarDecl>(*declStmt->decl_begin());
+}
+
+bool Flatten::TraverseCXXCatchStmt(CXXCatchStmt* )
+{
+ // ignore stuff inside catch statements, where doing a "if...else..throw" is more natural
+ return true;
+}
+
+bool Flatten::TraverseIfStmt(IfStmt * ifStmt)
+{
+ if (!WalkUpFromIfStmt(ifStmt)) {
+ return false;
+ }
+ auto const saved = mElseBranch;
+ mElseBranch = ifStmt->getElse();
+ auto ret = true;
+ for (auto const sub: ifStmt->children()) {
+ if (!TraverseStmt(sub)) {
+ ret = false;
+ break;
+ }
+ }
+ mElseBranch = saved;
+ return ret;
+}
+
+bool Flatten::TraverseCompoundStmt(CompoundStmt * compoundStmt)
+{
+ auto copy = lastStmtInCompoundStmt;
+ if (compoundStmt->size() > 0)
+ lastStmtInCompoundStmt = compoundStmt->body_back();
+ else
+ lastStmtInCompoundStmt = nullptr;
+
+ bool rv = RecursiveASTVisitor<Flatten>::TraverseCompoundStmt(compoundStmt);
+
+ lastStmtInCompoundStmt = copy;
+ return rv;
+}
+
+bool Flatten::TraverseFunctionDecl(FunctionDecl * fd)
+{
+ auto copy1 = functionDeclBody;
+ auto copy2 = fd;
+ functionDeclBody = dyn_cast_or_null<CompoundStmt>(fd->getBody());
+ functionDecl = fd;
+ bool rv = RecursiveASTVisitor<Flatten>::TraverseFunctionDecl(fd);
+ functionDeclBody = copy1;
+ functionDecl = copy2;
+ return rv;
+}
+
+bool Flatten::TraverseCXXMethodDecl(CXXMethodDecl * fd)
+{
+ auto copy1 = functionDeclBody;
+ auto copy2 = fd;
+ functionDeclBody = dyn_cast_or_null<CompoundStmt>(fd->getBody());
+ functionDecl = fd;
+ bool rv = RecursiveASTVisitor<Flatten>::TraverseCXXMethodDecl(fd);
+ functionDeclBody = copy1;
+ functionDecl = copy2;
+ return rv;
+}
+
+bool Flatten::TraverseCXXConstructorDecl(CXXConstructorDecl * fd)
+{
+ auto copy1 = functionDeclBody;
+ auto copy2 = fd;
+ functionDeclBody = dyn_cast_or_null<CompoundStmt>(fd->getBody());
+ functionDecl = fd;
+ bool rv = RecursiveASTVisitor<Flatten>::TraverseCXXConstructorDecl(fd);
+ functionDeclBody = copy1;
+ functionDecl = copy2;
+ return rv;
+}
+
+bool Flatten::TraverseCXXConversionDecl(CXXConversionDecl * fd)
+{
+ auto copy1 = functionDeclBody;
+ auto copy2 = fd;
+ functionDeclBody = dyn_cast_or_null<CompoundStmt>(fd->getBody());
+ functionDecl = fd;
+ bool rv = RecursiveASTVisitor<Flatten>::TraverseCXXConversionDecl(fd);
+ functionDeclBody = copy1;
+ functionDecl = copy2;
+ return rv;
+}
+
+bool Flatten::TraverseCXXDestructorDecl(CXXDestructorDecl * fd)
+{
+ auto copy1 = functionDeclBody;
+ auto copy2 = fd;
+ functionDeclBody = dyn_cast_or_null<CompoundStmt>(fd->getBody());
+ functionDecl = fd;
+ bool rv = RecursiveASTVisitor<Flatten>::TraverseCXXDestructorDecl(fd);
+ functionDeclBody = copy1;
+ functionDecl = copy2;
+ return rv;
+}
+
+
+bool Flatten::VisitIfStmt(IfStmt const * ifStmt)
+{
+ if (ignoreLocation(ifStmt))
+ return true;
+
+ // ignore if we are part of an if/then/else/if chain
+ if (ifStmt == mElseBranch || (ifStmt->getElse() && isa<IfStmt>(ifStmt->getElse())))
+ return true;
+
+ // look for a large if(){} block at the end of a function
+ if (!ifStmt->getElse()
+ && (functionDecl->getReturnType().isNull() || functionDecl->getReturnType()->isVoidType())
+ && functionDeclBody && functionDeclBody->size()
+ && functionDeclBody->body_back() == ifStmt
+ && isLargeCompoundStmt(ifStmt->getThen()))
+ {
+ if (!rewriteLargeIf(ifStmt))
+ {
+ report(
+ DiagnosticsEngine::Warning,
+ "large if statement at end of function, rather invert the condition and exit early, and flatten the function",
+ compat::getBeginLoc(ifStmt))
+ << ifStmt->getSourceRange();
+ }
+ return true;
+ }
+
+ if (!ifStmt->getElse())
+ return true;
+
+ auto const thenThrowExpr = containsSingleThrowExpr(ifStmt->getThen());
+ auto const elseThrowExpr = containsSingleThrowExpr(ifStmt->getElse());
+ // If neither contains a throw, nothing to do; if both contain throws, no
+ // improvement:
+ if ((thenThrowExpr == nullptr) == (elseThrowExpr == nullptr)) {
+ return true;
+ }
+
+ if (containsPreprocessingConditionalInclusion(ifStmt->getSourceRange())) {
+ return true;
+ }
+
+ if (elseThrowExpr)
+ {
+ // if the "if" statement is not the last statement in its block, and it contains
+ // var decls in its then block, we cannot de-indent the then block without
+ // extending the lifetime of some variables, which may be problematic
+ if (ifStmt != lastStmtInCompoundStmt && containsVarDecl(ifStmt->getThen()))
+ return true;
+
+ if (!rewrite1(ifStmt))
+ {
+ report(
+ DiagnosticsEngine::Warning,
+ "unconditional throw in else branch, rather invert the condition, throw early, and flatten the normal case",
+ compat::getBeginLoc(elseThrowExpr))
+ << elseThrowExpr->getSourceRange();
+ report(
+ DiagnosticsEngine::Note,
+ "if condition here",
+ compat::getBeginLoc(ifStmt))
+ << ifStmt->getSourceRange();
+ }
+ }
+ if (thenThrowExpr)
+ {
+ // if the "if" statement is not the last statement in its block, and it contains
+ // var decls in its else block, we cannot de-indent the else block without
+ // extending the lifetime of some variables, which may be problematic
+ if (ifStmt != lastStmtInCompoundStmt && containsVarDecl(ifStmt->getElse()))
+ return true;
+
+ if (!rewrite2(ifStmt))
+ {
+ report(
+ DiagnosticsEngine::Warning,
+ "unconditional throw in then branch, just flatten the else",
+ compat::getBeginLoc(thenThrowExpr))
+ << thenThrowExpr->getSourceRange();
+ }
+ }
+ return true;
+}
+
+static std::string stripOpenAndCloseBrace(std::string s);
+static std::string stripTrailingEmptyLines(std::string s);
+static std::string deindent(std::string const & s);
+static std::vector<std::string> split(std::string s);
+static bool startswith(std::string const & rStr, char const * pSubStr);
+static int countLeadingSpaces(std::string const &);
+static std::string padSpace(int iNoSpaces);
+static bool replace(std::string & s, std::string const & from, std::string const & to);
+
+bool Flatten::rewrite1(IfStmt const * ifStmt)
+{
+ if (!rewriter)
+ return false;
+
+ auto conditionRange = ignoreMacroExpansions(ifStmt->getCond()->getSourceRange());
+ if (!conditionRange.isValid()) {
+ return false;
+ }
+ auto thenRange = ignoreMacroExpansions(ifStmt->getThen()->getSourceRange());
+ if (!thenRange.isValid()) {
+ return false;
+ }
+ auto elseRange = ignoreMacroExpansions(ifStmt->getElse()->getSourceRange());
+ if (!elseRange.isValid()) {
+ return false;
+ }
+ SourceRange elseKeywordRange = ifStmt->getElseLoc();
+
+ thenRange = extendOverComments(thenRange);
+ elseRange = extendOverComments(elseRange);
+ elseKeywordRange = extendOverComments(elseKeywordRange);
+
+ // in adjusting the formatting I assume that "{" starts on a new line
+
+ llvm::Optional<std::string> conditionString = invertCondition(ifStmt->getCond(), conditionRange);
+ if (!conditionString)
+ return false;
+
+ std::string thenString = getSourceAsString(thenRange);
+ if (auto compoundStmt = dyn_cast<CompoundStmt>(ifStmt->getThen())) {
+ if (compoundStmt->getLBracLoc().isValid()) {
+ thenString = stripOpenAndCloseBrace(thenString);
+ }
+ }
+ thenString = deindent(thenString);
+
+ std::string elseString = getSourceAsString(elseRange);
+
+ if (!replaceText(elseRange, thenString)) {
+ return false;
+ }
+ if (!removeText(elseKeywordRange)) {
+ return false;
+ }
+ if (!replaceText(thenRange, elseString)) {
+ return false;
+ }
+ if (!replaceText(conditionRange, *conditionString)) {
+ return false;
+ }
+
+ return true;
+}
+
+bool Flatten::rewrite2(IfStmt const * ifStmt)
+{
+ if (!rewriter)
+ return false;
+
+ auto conditionRange = ignoreMacroExpansions(ifStmt->getCond()->getSourceRange());
+ if (!conditionRange.isValid()) {
+ return false;
+ }
+ auto thenRange = ignoreMacroExpansions(ifStmt->getThen()->getSourceRange());
+ if (!thenRange.isValid()) {
+ return false;
+ }
+ auto elseRange = ignoreMacroExpansions(ifStmt->getElse()->getSourceRange());
+ if (!elseRange.isValid()) {
+ return false;
+ }
+ SourceRange elseKeywordRange = ifStmt->getElseLoc();
+
+ elseRange = extendOverComments(elseRange);
+ elseKeywordRange = extendOverComments(elseKeywordRange);
+
+ // in adjusting the formatting I assume that "{" starts on a new line
+
+ std::string elseString = getSourceAsString(elseRange);
+ if (auto compoundStmt = dyn_cast<CompoundStmt>(ifStmt->getElse())) {
+ if (compoundStmt->getLBracLoc().isValid()) {
+ elseString = stripOpenAndCloseBrace(elseString);
+ }
+ }
+ elseString = deindent(elseString);
+
+ if (!replaceText(elseRange, elseString)) {
+ return false;
+ }
+ if (!removeText(elseKeywordRange)) {
+ return false;
+ }
+
+ return true;
+}
+
+bool Flatten::rewriteLargeIf(IfStmt const * ifStmt)
+{
+ if (!rewriter)
+ return false;
+
+ auto conditionRange = ignoreMacroExpansions(ifStmt->getCond()->getSourceRange());
+ if (!conditionRange.isValid()) {
+ return false;
+ }
+ auto thenRange = ignoreMacroExpansions(ifStmt->getThen()->getSourceRange());
+ if (!thenRange.isValid()) {
+ return false;
+ }
+
+ thenRange = extendOverComments(thenRange);
+
+ // in adjusting the formatting I assume that "{" starts on a new line
+
+ llvm::Optional<std::string> conditionString = invertCondition(ifStmt->getCond(), conditionRange);
+ if (!conditionString)
+ return false;
+
+ std::string thenString = getSourceAsString(thenRange);
+ if (auto compoundStmt = dyn_cast<CompoundStmt>(ifStmt->getThen())) {
+ if (compoundStmt->getLBracLoc().isValid()) {
+ thenString = stripOpenAndCloseBrace(thenString);
+ }
+ }
+ int iNoSpaces = countLeadingSpaces(thenString);
+ thenString = padSpace(iNoSpaces) + "return;\n\n" + deindent(thenString);
+ thenString = stripTrailingEmptyLines(thenString);
+
+ if (!replaceText(thenRange, thenString)) {
+ return false;
+ }
+ if (!replaceText(conditionRange, *conditionString)) {
+ return false;
+ }
+
+ return true;
+}
+
+llvm::Optional<std::string> Flatten::invertCondition(Expr const * condExpr, SourceRange conditionRange)
+{
+ std::string s = getSourceAsString(conditionRange);
+
+ condExpr = condExpr->IgnoreImpCasts();
+
+ if (auto exprWithCleanups = dyn_cast<ExprWithCleanups>(condExpr))
+ condExpr = exprWithCleanups->getSubExpr()->IgnoreImpCasts();
+
+ // an if statement will automatically invoke a bool-conversion method
+ if (auto memberCallExpr = dyn_cast<CXXMemberCallExpr>(condExpr))
+ {
+ if (isa<CXXConversionDecl>(memberCallExpr->getMethodDecl()))
+ condExpr = memberCallExpr->getImplicitObjectArgument()->IgnoreImpCasts();
+ }
+
+ if (auto unaryOp = dyn_cast<UnaryOperator>(condExpr))
+ {
+ if (unaryOp->getOpcode() != UO_LNot)
+ return "!(" + s + ")";
+ auto i = s.find("!");
+ assert (i != std::string::npos);
+ s = s.substr(i+1);
+ }
+ else if (auto binaryOp = dyn_cast<BinaryOperator>(condExpr))
+ {
+ bool ok = true;
+ switch (binaryOp->getOpcode())
+ {
+ case BO_LT: ok = replace(s, "<", ">="); break;
+ case BO_GT: ok = replace(s, ">", "<="); break;
+ case BO_LE: ok = replace(s, "<=", ">"); break;
+ case BO_GE: ok = replace(s, ">=", "<"); break;
+ case BO_EQ: ok = replace(s, "==", "!="); break;
+ case BO_NE: ok = replace(s, "!=", "=="); break;
+ default:
+ s = "!(" + s + ")";
+ }
+ if (!ok)
+ return llvm::Optional<std::string>();
+ }
+ else if (auto opCallExpr = dyn_cast<CXXOperatorCallExpr>(condExpr))
+ {
+ bool ok = true;
+ switch (opCallExpr->getOperator())
+ {
+ case OO_Less: ok = replace(s, "<", ">="); break;
+ case OO_Greater: ok = replace(s, ">", "<="); break;
+ case OO_LessEqual: ok = replace(s, "<=", ">"); break;
+ case OO_GreaterEqual: ok = replace(s, ">=", "<"); break;
+ case OO_EqualEqual: ok = replace(s, "==", "!="); break;
+ case OO_ExclaimEqual: ok = replace(s, "!=", "=="); break;
+ default:
+ s = "!(" + s + ")";
+ }
+ if (!ok)
+ return llvm::Optional<std::string>();
+ }
+ else if (isa<DeclRefExpr>(condExpr) || isa<CallExpr>(condExpr) || isa<MemberExpr>(condExpr))
+ s = "!" + s;
+ else
+ s = "!(" + s + ")";
+ return s;
+}
+
+std::string stripOpenAndCloseBrace(std::string s)
+{
+ size_t i = s.find("{");
+ if (i == std::string::npos)
+ {
+ assert( !"did not find {" );
+ abort();
+ }
+
+ ++i;
+ // strip to line end
+ while (s[i] == ' ')
+ ++i;
+ if (s[i] == '\n')
+ ++i;
+ s = s.substr(i);
+
+ i = s.rfind("}");
+ if (i == std::string::npos)
+ {
+ assert( !"did not find }" );
+ abort();
+ }
+ --i;
+ while (s[i] == ' ')
+ --i;
+ s = s.substr(0,i);
+ return s;
+}
+
+std::string deindent(std::string const & s)
+{
+ std::vector<std::string> lines = split(s);
+ std::string rv;
+ for (auto s : lines) {
+ if (startswith(s, " "))
+ rv += s.substr(4);
+ else
+ rv += s;
+ rv += "\n";
+ }
+ return rv;
+}
+
+std::vector<std::string> split(std::string s)
+{
+ if (s.back() == '\n')
+ s = s.substr(0, s.size()-1);
+ size_t next = -1;
+ std::vector<std::string> rv;
+ do
+ {
+ size_t current = next + 1;
+ next = s.find_first_of( "\n", current );
+ rv.push_back(s.substr( current, next - current ));
+ }
+ while (next != std::string::npos);
+ return rv;
+}
+
+bool startswith(std::string const & rStr, char const * pSubStr)
+{
+ return rStr.compare(0, strlen(pSubStr), pSubStr) == 0;
+}
+
+int countLeadingSpaces(std::string const & s)
+{
+ int i = 0;
+ while (i < (int)s.length() && s[i] == ' ')
+ i++;
+ return i;
+}
+
+std::string padSpace(int iNoSpaces)
+{
+ std::string s;
+ for (int i = 0; i < iNoSpaces; ++i)
+ s += " ";
+ return s;
+}
+
+std::string stripTrailingEmptyLines(std::string s)
+{
+ while (s.back() == '\n')
+ s.resize(s.length() - 1);
+ return s;
+}
+
+bool replace(std::string & s, std::string const & from, std::string const & to)
+{
+ auto i = s.find(from);
+ assert (i != std::string::npos);
+ s.replace(i, from.length(), to);
+ // just in case we have something really weird, like the operator token is also present in the rest of the condition somehow
+ return s.find(from) == std::string::npos;
+}
+
+SourceRange Flatten::ignoreMacroExpansions(SourceRange range) {
+ while (compiler.getSourceManager().isMacroArgExpansion(range.getBegin())) {
+ range.setBegin(
+ compiler.getSourceManager().getImmediateMacroCallerLoc(
+ range.getBegin()));
+ }
+ if (range.getBegin().isMacroID()) {
+ SourceLocation loc;
+ if (Lexer::isAtStartOfMacroExpansion(
+ range.getBegin(), compiler.getSourceManager(),
+ compiler.getLangOpts(), &loc))
+ {
+ range.setBegin(loc);
+ }
+ }
+ while (compiler.getSourceManager().isMacroArgExpansion(range.getEnd())) {
+ range.setEnd(
+ compiler.getSourceManager().getImmediateMacroCallerLoc(
+ range.getEnd()));
+ }
+ if (range.getEnd().isMacroID()) {
+ SourceLocation loc;
+ if (Lexer::isAtEndOfMacroExpansion(
+ range.getEnd(), compiler.getSourceManager(),
+ compiler.getLangOpts(), &loc))
+ {
+ range.setEnd(loc);
+ }
+ }
+ return range.getBegin().isMacroID() || range.getEnd().isMacroID()
+ ? SourceRange() : range;
+}
+
+/**
+ * Extend the SourceRange to include any leading and trailing whitespace, and any comments.
+ */
+SourceRange Flatten::extendOverComments(SourceRange range)
+{
+ SourceManager& SM = compiler.getSourceManager();
+ SourceLocation startLoc = range.getBegin();
+ SourceLocation endLoc = range.getEnd();
+ char const *p1 = SM.getCharacterData( startLoc );
+ char const *p2 = SM.getCharacterData( endLoc );
+
+ // scan backwards from the beginning to include any spaces on that line
+ while (*(p1-1) == ' ')
+ --p1;
+ startLoc = startLoc.getLocWithOffset(p1 - SM.getCharacterData( startLoc ));
+
+ // look for trailing ";"
+ while (*(p2+1) == ';')
+ ++p2;
+ // look for trailing " "
+ while (*(p2+1) == ' ')
+ ++p2;
+ // look for single line comments attached to the end of the statement
+ if (*(p2+1) == '/' && *(p2+2) == '/')
+ {
+ p2 += 2;
+ while (*(p2+1) && *(p2+1) != '\n')
+ ++p2;
+ if (*(p2+1) == '\n')
+ ++p2;
+ }
+ else
+ {
+ // make the source code we extract include any trailing "\n"
+ if (*(p2+1) == '\n')
+ ++p2;
+ }
+ endLoc = endLoc.getLocWithOffset(p2 - SM.getCharacterData( endLoc ));
+
+ return SourceRange(startLoc, endLoc);
+}
+
+std::string Flatten::getSourceAsString(SourceRange range)
+{
+ SourceManager& SM = compiler.getSourceManager();
+ SourceLocation startLoc = range.getBegin();
+ SourceLocation endLoc = range.getEnd();
+ char const *p1 = SM.getCharacterData( startLoc );
+ char const *p2 = SM.getCharacterData( endLoc );
+ p2 += Lexer::MeasureTokenLength( endLoc, SM, compiler.getLangOpts());
+ if (p2 < p1) {
+ // workaround clang weirdness, but don't return empty string
+ // in case it happens during code replacement
+ return "clang returned bad pointers";
+ }
+ return std::string( p1, p2 - p1);
+}
+
+bool Flatten::isLargeCompoundStmt(Stmt const * stmt)
+{
+ auto stmtRange = stmt->getSourceRange();
+ std::string s = getSourceAsString(stmtRange);
+ return std::count(s.begin(), s.end(), '\n') > 10;
+}
+
+loplugin::Plugin::Registration< Flatten > X("flatten", false);
+
+}
+
+/* vim:set shiftwidth=4 softtabstop=4 expandtab: */