/* -*- 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/. */ #ifndef LO_CLANG_SHARED_PLUGINS #include #include #include #include #include "plugin.hxx" #include "check.hxx" #include "compat.hxx" #include "config_clang.h" #include "clang/AST/CXXInheritance.h" #include "clang/AST/StmtVisitor.h" /** Look for *StringBuffer append sequences which can be converted to *String + sequences. */ namespace { class BufferAdd : public loplugin::FilteringPlugin { public: explicit BufferAdd(loplugin::InstantiationData const& data) : FilteringPlugin(data) { } bool preRun() override { std::string fn(handler.getMainFileName()); loplugin::normalizeDotDotInFilePath(fn); if (loplugin::hasPathnamePrefix(fn, SRCDIR "/sal/qa/rtl/oustring/")) return false; if (loplugin::hasPathnamePrefix(fn, SRCDIR "/sal/qa/rtl/oustringbuffer/")) return false; if (loplugin::hasPathnamePrefix(fn, SRCDIR "/sal/qa/rtl/strings/")) return false; if (loplugin::hasPathnamePrefix(fn, SRCDIR "/sal/qa/OStringBuffer/")) return false; // some false + if (loplugin::isSamePathname(fn, SRCDIR "/unoidl/source/sourcetreeprovider.cxx")) return false; if (loplugin::isSamePathname(fn, SRCDIR "/sw/source/writerfilter/dmapper/StyleSheetTable.cxx")) return false; if (loplugin::isSamePathname(fn, SRCDIR "/sw/source/writerfilter/dmapper/GraphicImport.cxx")) return false; if (loplugin::isSamePathname(fn, SRCDIR "/sdext/source/pdfimport/pdfparse/pdfparse.cxx")) return false; return true; } void postRun() override { for (auto const& pair : goodMap) if (!isa(pair.first) && // reference types have slightly weird behaviour !pair.first->getType()->isReferenceType() && badMap.find(pair.first) == badMap.end()) report(DiagnosticsEngine::Warning, "convert this append sequence into a *String + sequence", pair.first->getBeginLoc()) << pair.first->getSourceRange(); } virtual void run() override { if (!preRun()) return; TraverseDecl(compiler.getASTContext().getTranslationUnitDecl()); postRun(); } bool VisitStmt(Stmt const*); bool VisitCallExpr(CallExpr const*); bool VisitCXXConstructExpr(CXXConstructExpr const*); bool VisitUnaryOperator(UnaryOperator const*); private: void findBufferAssignOrAdd(const Stmt* parentStmt, Stmt const*); Expr const* ignore(Expr const*); bool isSideEffectFree(Expr const*); bool isMethodOkToMerge(CXXMemberCallExpr const*); void addToGoodMap(const VarDecl* varDecl, const Stmt* parentStmt); std::unordered_map goodMap; std::unordered_set badMap; }; bool BufferAdd::VisitStmt(Stmt const* stmt) { if (ignoreLocation(stmt)) return true; if (!isa(stmt) && !isa(stmt) && !isa(stmt) && !isa(stmt) && !isa(stmt) && !isa(stmt) && !isa(stmt) && !isa(stmt) && !isa(stmt)) return true; for (auto it = stmt->child_begin(); it != stmt->child_end(); ++it) if (*it) findBufferAssignOrAdd(stmt, *it); return true; } bool BufferAdd::VisitCallExpr(CallExpr const* callExpr) { if (ignoreLocation(callExpr)) return true; for (unsigned i = 0; i != callExpr->getNumArgs(); ++i) { auto a = ignore(callExpr->getArg(i)); if (auto declRefExpr = dyn_cast(a)) if (auto varDecl = dyn_cast(declRefExpr->getDecl())) badMap.insert(varDecl); } return true; } bool BufferAdd::VisitCXXConstructExpr(CXXConstructExpr const* callExpr) { if (ignoreLocation(callExpr)) return true; for (unsigned i = 0; i != callExpr->getNumArgs(); ++i) { auto a = ignore(callExpr->getArg(i)); if (auto declRefExpr = dyn_cast(a)) if (auto varDecl = dyn_cast(declRefExpr->getDecl())) badMap.insert(varDecl); } return true; } bool BufferAdd::VisitUnaryOperator(const UnaryOperator* unaryOp) { if (ignoreLocation(unaryOp)) return true; if (unaryOp->getOpcode() != UO_AddrOf) return true; auto a = ignore(unaryOp->getSubExpr()); if (auto declRefExpr = dyn_cast(a)) if (auto varDecl = dyn_cast(declRefExpr->getDecl())) badMap.insert(varDecl); return true; } void BufferAdd::findBufferAssignOrAdd(const Stmt* parentStmt, Stmt const* stmt) { if (auto exprCleanup = dyn_cast(stmt)) stmt = exprCleanup->getSubExpr(); if (auto switchCase = dyn_cast(stmt)) stmt = switchCase->getSubStmt(); if (auto declStmt = dyn_cast(stmt)) { if (declStmt->isSingleDecl()) if (auto varDeclLHS = dyn_cast_or_null(declStmt->getSingleDecl())) { auto tc = loplugin::TypeCheck(varDeclLHS->getType()); if (!tc.Class("OUStringBuffer").Namespace("rtl").GlobalNamespace() && !tc.Class("OStringBuffer").Namespace("rtl").GlobalNamespace()) return; if (varDeclLHS->getStorageDuration() == SD_Static) return; if (!varDeclLHS->hasInit()) return; auto cxxConstructExpr = dyn_cast(ignore(varDeclLHS->getInit())); if (cxxConstructExpr) { addToGoodMap(varDeclLHS, parentStmt); return; } if (!isSideEffectFree(varDeclLHS->getInit())) badMap.insert(varDeclLHS); else addToGoodMap(varDeclLHS, parentStmt); } return; } // check for single calls to buffer method if (auto memberCallExpr = dyn_cast(stmt)) { if (auto declRefExprLHS = dyn_cast(ignore(memberCallExpr->getImplicitObjectArgument()))) { auto methodDecl = memberCallExpr->getMethodDecl(); if (methodDecl && methodDecl->getIdentifier()) if (auto varDeclLHS = dyn_cast(declRefExprLHS->getDecl())) { auto tc = loplugin::TypeCheck(varDeclLHS->getType()); if (tc.Class("OUStringBuffer").Namespace("rtl").GlobalNamespace() || tc.Class("OStringBuffer").Namespace("rtl").GlobalNamespace()) { if (isMethodOkToMerge(memberCallExpr)) addToGoodMap(varDeclLHS, parentStmt); else badMap.insert(varDeclLHS); } } return; } } // now check for chained append calls auto expr = dyn_cast(stmt); if (!expr) return; auto tc = loplugin::TypeCheck(expr->getType()); if (!tc.Class("OUStringBuffer").Namespace("rtl").GlobalNamespace() && !tc.Class("OStringBuffer").Namespace("rtl").GlobalNamespace()) return; // unwrap the chain (which runs from right to left) const VarDecl* varDeclLHS = nullptr; bool good = true; while (true) { auto memberCallExpr = dyn_cast(expr); if (!memberCallExpr) break; good &= isMethodOkToMerge(memberCallExpr); if (auto declRefExprLHS = dyn_cast(ignore(memberCallExpr->getImplicitObjectArgument()))) { varDeclLHS = dyn_cast(declRefExprLHS->getDecl()); break; } expr = memberCallExpr->getImplicitObjectArgument(); } if (varDeclLHS) { if (good) addToGoodMap(varDeclLHS, parentStmt); else badMap.insert(varDeclLHS); } } void BufferAdd::addToGoodMap(const VarDecl* varDecl, const Stmt* parentStmt) { // check that vars are all inside the same compoundstmt, if they are not, we cannot combine them auto it = goodMap.find(varDecl); if (it != goodMap.end()) { if (it->second == parentStmt) return; // don't treat these as parents, otherwise we eliminate .append.append sequences if (isa(parentStmt)) return; if (isa(parentStmt)) return; badMap.insert(varDecl); } else goodMap.emplace(varDecl, parentStmt); } bool BufferAdd::isMethodOkToMerge(CXXMemberCallExpr const* memberCall) { auto methodDecl = memberCall->getMethodDecl(); if (methodDecl->getNumParams() == 0) return true; if (auto const id = methodDecl->getIdentifier()) { auto name = id->getName(); if (name == "appendUninitialized" || name == "setLength" || name == "remove" || name == "insert" || name == "appendAscii" || name == "appendUtf32") return false; } auto rhs = memberCall->getArg(0); if (!isSideEffectFree(rhs)) return false; return true; } Expr const* BufferAdd::ignore(Expr const* expr) { return expr->IgnoreImplicit()->IgnoreParens()->IgnoreImplicit(); } bool BufferAdd::isSideEffectFree(Expr const* expr) { expr = ignore(expr); // I don't think the OUStringAppend functionality can handle this efficiently if (isa(expr)) return false; // Multiple statements have a well defined evaluation order (sequence points between them) // but a single expression may be evaluated in arbitrary order; // if there are side effects in one of the sub-expressions that have an effect on another subexpression, // the result may be incorrect, and you don't necessarily notice in tests because the order is compiler-dependent. // for example see commit afd743141f7a7dd05914d0872c9afe079f16fe0c where such a refactoring introduced such a bug. // So only consider simple RHS expressions. if (!expr->HasSideEffects(compiler.getASTContext())) return true; // check for chained adds which are side-effect free if (auto operatorCall = dyn_cast(expr)) { auto op = operatorCall->getOperator(); if (op == OO_PlusEqual || op == OO_Plus) if (isSideEffectFree(operatorCall->getArg(0)) && isSideEffectFree(operatorCall->getArg(1))) return true; } if (auto callExpr = dyn_cast(expr)) { // check for calls through OUString::number/OUString::unacquired if (auto calleeMethodDecl = dyn_cast_or_null(callExpr->getCalleeDecl())) if (calleeMethodDecl && calleeMethodDecl->getIdentifier()) { if (callExpr->getNumArgs() > 0) { auto tc = loplugin::TypeCheck(calleeMethodDecl->getParent()); if (tc.Class("OUString") || tc.Class("OString")) { if (isSideEffectFree(callExpr->getArg(0))) return true; } } } if (auto calleeFunctionDecl = dyn_cast_or_null(callExpr->getCalleeDecl())) if (calleeFunctionDecl && calleeFunctionDecl->getIdentifier()) { auto name = calleeFunctionDecl->getName(); // check for calls through OUStringToOString if (name == "OUStringToOString" || name == "OStringToOUString") if (isSideEffectFree(callExpr->getArg(0))) return true; // allowlist some known-safe methods if (compat::ends_with(name, "ResId") || name == "GetXMLToken") if (isSideEffectFree(callExpr->getArg(0))) return true; } // O[U]String::operator std::[u16]string_view: if (auto const d = dyn_cast_or_null(callExpr->getCalleeDecl())) { auto tc = loplugin::TypeCheck(d->getParent()); if (tc.Class("OString") || tc.Class("OUString")) { return true; } } } // sometimes we have a constructor call on the RHS if (auto constructExpr = dyn_cast(expr)) { auto dc = loplugin::DeclCheck(constructExpr->getConstructor()); if (dc.MemberFunction().Class("OUString") || dc.MemberFunction().Class("OString") || dc.MemberFunction().Class("OUStringBuffer") || dc.MemberFunction().Class("OStringBuffer")) if (constructExpr->getNumArgs() == 0 || isSideEffectFree(constructExpr->getArg(0))) return true; // Expr::HasSideEffects does not like stuff that passes through OUStringLiteral auto dc2 = loplugin::DeclCheck(constructExpr->getConstructor()->getParent()); if (dc2.Class("OUStringLiteral").Namespace("rtl").GlobalNamespace()) return true; } // when adding literals, we sometimes get this if (auto functionalCastExpr = dyn_cast(expr)) { auto tc = loplugin::TypeCheck(functionalCastExpr->getType()); if (tc.Class("OUStringLiteral").Namespace("rtl").GlobalNamespace()) return isSideEffectFree(functionalCastExpr->getSubExpr()); } return false; } loplugin::Plugin::Registration bufferadd("bufferadd"); } #endif // LO_CLANG_SHARED_PLUGINS /* vim:set shiftwidth=4 softtabstop=4 expandtab: */