diff options
Diffstat (limited to 'compilerplugins/clang/constmethod.cxx')
-rw-r--r-- | compilerplugins/clang/constmethod.cxx | 563 |
1 files changed, 563 insertions, 0 deletions
diff --git a/compilerplugins/clang/constmethod.cxx b/compilerplugins/clang/constmethod.cxx new file mode 100644 index 0000000000..bd9c4db18d --- /dev/null +++ b/compilerplugins/clang/constmethod.cxx @@ -0,0 +1,563 @@ +/* -*- 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 <algorithm> +#include <string> +#include <unordered_set> +#include <unordered_map> +#include <iostream> + +#include "config_clang.h" + +#include "plugin.hxx" +#include "check.hxx" +#include "functionaddress.hxx" + +#include "clang/AST/ParentMapContext.h" + +/** + Find methods that can be declared const. + + This analysis attempts to implement "logical const" as opposed to "technical const", which means + we ignore always-const nature of std::unique_ptr::operator-> + + This is not a sophisticated analysis. It deliberately skips all of the hard cases for now. + It is an exercise in getting the most benefit for the least effort. +*/ +namespace +{ + +class ConstMethod: + public loplugin::FunctionAddress<loplugin::FilteringPlugin<ConstMethod>> +{ +public: + explicit ConstMethod(loplugin::InstantiationData const & data): FunctionAddress(data) {} + + virtual void run() override { + std::string fn(handler.getMainFileName()); + loplugin::normalizeDotDotInFilePath(fn); + // things I'm not sure about + if (loplugin::hasPathnamePrefix(fn, SRCDIR "/svl/unx/source/svdde/ddedummy.cxx") + || loplugin::hasPathnamePrefix(fn, SRCDIR "/svl/source/numbers/zformat.cxx") + || loplugin::hasPathnamePrefix(fn, SRCDIR "/svl/source/numbers/zforscan.cxx") + || loplugin::hasPathnamePrefix(fn, SRCDIR "/svl/source/numbers/zforlist.cxx") + || loplugin::hasPathnamePrefix(fn, SRCDIR "/vcl/source/gdi/impgraph.cxx") + || loplugin::hasPathnamePrefix(fn, SRCDIR "/vcl/source/image/ImplImage.cxx") + || loplugin::hasPathnamePrefix(fn, SRCDIR "/vcl/source/filter/wmf/wmfwr.cxx") + || loplugin::hasPathnamePrefix(fn, SRCDIR "/vcl/unx/generic/app/i18n_im.cxx") + || loplugin::hasPathnamePrefix(fn, SRCDIR "/vcl/unx/generic/app/randrwrapper.cxx") + || loplugin::hasPathnamePrefix(fn, SRCDIR "/vcl/unx/gtk3/gtkinst.cxx") + || loplugin::hasPathnamePrefix(fn, SRCDIR "/vcl/unx/gtk3/gtkframe.cxx") + || loplugin::hasPathnamePrefix(fn, SRCDIR "/vcl/skia/gdiimpl.cxx") + || loplugin::hasPathnamePrefix(fn, SRCDIR "/vcl/qt5/") + || loplugin::hasPathnamePrefix(fn, SRCDIR "/package/source/xstor/owriteablestream.cxx") + || loplugin::hasPathnamePrefix(fn, SRCDIR "/package/source/zippackage/ZipPackage.cxx") + || loplugin::hasPathnamePrefix(fn, SRCDIR "/toolkit/") + || loplugin::hasPathnamePrefix(fn, SRCDIR "/canvas/") + || loplugin::hasPathnamePrefix(fn, SRCDIR "/accessibility/") + || loplugin::hasPathnamePrefix(fn, SRCDIR "/framework/") + || loplugin::hasPathnamePrefix(fn, SRCDIR "/basic/") + || loplugin::hasPathnamePrefix(fn, SRCDIR "/sfx2/") + || loplugin::hasPathnamePrefix(fn, SRCDIR "/xmloff/") + || loplugin::hasPathnamePrefix(fn, SRCDIR "/connectivity/") + || loplugin::hasPathnamePrefix(fn, SRCDIR "/editeng/") + || loplugin::hasPathnamePrefix(fn, SRCDIR "/scripting/") + || loplugin::hasPathnamePrefix(fn, SRCDIR "/ucb/") + || loplugin::hasPathnamePrefix(fn, SRCDIR "/svx/") + || loplugin::hasPathnamePrefix(fn, SRCDIR "/basctl/") + || loplugin::hasPathnamePrefix(fn, SRCDIR "/chart2/") + ) + return; + + TraverseDecl(compiler.getASTContext().getTranslationUnitDecl()); + + for (const CXXMethodDecl *pMethodDecl : interestingMethodSet) { + if (methodCannotBeConstSet.find(pMethodDecl) != methodCannotBeConstSet.end()) + continue; + auto canonicalDecl = pMethodDecl->getCanonicalDecl(); + if (getFunctionsWithAddressTaken().find((FunctionDecl const *)canonicalDecl) + != getFunctionsWithAddressTaken().end()) + continue; + // things that I don't think should be logically const + std::string fqn = pMethodDecl->getQualifiedNameAsString(); + if (fqn == "comphelper::EmbeddedObjectContainer::CommitImageSubStorage" + || fqn == "SvtLinguConfig::SetProperty" + || fqn == "SvtLinguConfig::ReplaceSetProperties" + || fqn == "SystemWindow::UpdatePositionData" + || fqn == "OutputDevice::SelectClipRegion" + || fqn == "OutputDevice::BlendBitmap") + continue; + StringRef aFileName = getFilenameOfLocation(compiler.getSourceManager().getSpellingLoc(canonicalDecl->getBeginLoc())); + // leave the kit API alone + if (loplugin::isSamePathname(aFileName, SRCDIR "/include/LibreOfficeKit/LibreOfficeKit.hxx")) + continue; + // don't feel like touching this right now + if (loplugin::isSamePathname(aFileName, SRCDIR "/include/vcl/weld.hxx")) + continue; + report( + DiagnosticsEngine::Warning, + "this method can be const", + pMethodDecl->getBeginLoc()) + << pMethodDecl->getSourceRange(); + if (canonicalDecl->getLocation() != pMethodDecl->getLocation()) { + report( + DiagnosticsEngine::Note, + "canonical method declaration here", + canonicalDecl->getBeginLoc()) + << canonicalDecl->getSourceRange(); + } + } + } + + bool TraverseCXXMethodDecl(CXXMethodDecl *); + bool TraverseCXXConversionDecl(CXXConversionDecl *); + bool VisitCXXMethodDecl(const CXXMethodDecl *); + bool VisitCXXThisExpr(const CXXThisExpr *); + +private: + bool isPointerOrReferenceToConst(const QualType& qt); + bool isPointerOrReferenceToNonConst(const QualType& qt); + bool checkIfCanBeConst(const Stmt*, const CXXMethodDecl*); + + std::unordered_set<const CXXMethodDecl*> interestingMethodSet; + std::unordered_set<const CXXMethodDecl*> methodCannotBeConstSet; + CXXMethodDecl const * currCXXMethodDecl; +}; + +bool ConstMethod::TraverseCXXMethodDecl(CXXMethodDecl * cxxMethodDecl) +{ + currCXXMethodDecl = cxxMethodDecl; + bool rv = RecursiveASTVisitor<ConstMethod>::TraverseCXXMethodDecl(cxxMethodDecl); + currCXXMethodDecl = nullptr; + return rv; +} + +bool ConstMethod::TraverseCXXConversionDecl(CXXConversionDecl * cxxConversionDecl) +{ + currCXXMethodDecl = cxxConversionDecl; + bool rv = RecursiveASTVisitor<ConstMethod>::TraverseCXXConversionDecl(cxxConversionDecl); + currCXXMethodDecl = nullptr; + return rv; +} + +bool ConstMethod::VisitCXXMethodDecl(const CXXMethodDecl * cxxMethodDecl) +{ + if (ignoreLocation(cxxMethodDecl) || !cxxMethodDecl->isThisDeclarationADefinition()) { + return true; + } + if (cxxMethodDecl->isConst()) + return true; + // ignore stuff that forms part of the stable URE interface + if (isInUnoIncludeFile(cxxMethodDecl)) { + return true; + } + // TODO ignore template stuff for now + if (cxxMethodDecl->getTemplatedKind() != FunctionDecl::TK_NonTemplate) { + return true; + } + if (cxxMethodDecl->isDeleted()) + return true; + if (cxxMethodDecl->isStatic()) + return true; + if (cxxMethodDecl->isOverloadedOperator()) + return true; + if (isa<CXXConstructorDecl>(cxxMethodDecl)) + return true; + if (isa<CXXDestructorDecl>(cxxMethodDecl)) + return true; + if (cxxMethodDecl->getParent()->getDescribedClassTemplate() != nullptr ) { + return true; + } + // ignore virtual methods + if (cxxMethodDecl->isVirtual() ) { + return true; + } + // ignore macro expansions so we can ignore the IMPL_LINK macros from include/tools/link.hxx + // TODO make this more precise + if (cxxMethodDecl->getLocation().isMacroID()) + return true; + + if (!cxxMethodDecl->getIdentifier()) + return true; +// if (cxxMethodDecl->getNumParams() > 0) +// return true; + // returning pointers or refs to non-const stuff, and then having the whole method + // be const doesn't seem like a good idea + auto tc = loplugin::TypeCheck(cxxMethodDecl->getReturnType()); + if (tc.Pointer().NonConst()) + return true; + if (tc.LvalueReference().NonConst()) + return true; + // a Get method that returns void is probably doing something that has side-effects + if (tc.Void()) + return true; + +// StringRef name = cxxMethodDecl->getName(); +// if (!name.startswith("get") && !name.startswith("Get") +// && !name.startswith("is") && !name.startswith("Is") +// && !name.startswith("has") && !name.startswith("Has")) +// return true; + + // something lacking in my analysis here + if (loplugin::DeclCheck(cxxMethodDecl).Function("GetDescr").Class("SwRangeRedline").GlobalNamespace()) + return true; + + interestingMethodSet.insert(cxxMethodDecl); + + return true; +} + +bool ConstMethod::VisitCXXThisExpr( const CXXThisExpr* cxxThisExpr ) +{ + if (!currCXXMethodDecl) + return true; + if (ignoreLocation(cxxThisExpr)) + return true; + // ignore stuff that forms part of the stable URE interface + if (isInUnoIncludeFile(cxxThisExpr->getBeginLoc())) + return true; + if (interestingMethodSet.find(currCXXMethodDecl) == interestingMethodSet.end()) + return true; + // no need to check again if we have already eliminated this one + if (methodCannotBeConstSet.find(currCXXMethodDecl) != methodCannotBeConstSet.end()) + return true; + if (!checkIfCanBeConst(cxxThisExpr, currCXXMethodDecl)) + methodCannotBeConstSet.insert(currCXXMethodDecl); + + return true; +} + +// Walk up from a statement that contains a CXXThisExpr, checking if the usage means that the +// related CXXMethodDecl can be const. +bool ConstMethod::checkIfCanBeConst(const Stmt* stmt, const CXXMethodDecl* cxxMethodDecl) +{ + const Stmt* parent = getParentStmt( stmt ); + if (!parent) { + auto parentsRange = compiler.getASTContext().getParents(*stmt); + if ( parentsRange.begin() == parentsRange.end()) + return true; + auto varDecl = dyn_cast_or_null<VarDecl>(parentsRange.begin()->get<Decl>()); + if (!varDecl) + { + report( + DiagnosticsEngine::Warning, + "no parent?", + stmt->getBeginLoc()) + << stmt->getSourceRange(); + return false; + } + return varDecl->getType()->isIntegralOrEnumerationType() + || loplugin::TypeCheck(varDecl->getType()).Pointer().Const() + || loplugin::TypeCheck(varDecl->getType()).LvalueReference().Const(); + } + + if (auto unaryOperator = dyn_cast<UnaryOperator>(parent)) { + UnaryOperator::Opcode op = unaryOperator->getOpcode(); + if (op == UO_PreInc || op == UO_PostInc + || op == UO_PreDec || op == UO_PostDec) { + return false; + } + if (op == UO_Deref || op == UO_AddrOf) { + return checkIfCanBeConst(parent, cxxMethodDecl); + } + return true; + } else if (auto binaryOp = dyn_cast<BinaryOperator>(parent)) { + BinaryOperator::Opcode op = binaryOp->getOpcode(); + if (binaryOp->getRHS() == stmt) { + return true; + } + if (op == BO_Assign || op == BO_PtrMemD || op == BO_PtrMemI || op == BO_MulAssign + || op == BO_DivAssign || op == BO_RemAssign || op == BO_AddAssign + || op == BO_SubAssign || op == BO_ShlAssign || op == BO_ShrAssign + || op == BO_AndAssign || op == BO_XorAssign || op == BO_OrAssign) { + return false; + } +// // for pointer arithmetic need to check parent +// if (binaryOp->getType()->isPointerType()) { +// return checkIfCanBeConst(parent, cxxMethodDecl); +// } + return true; + } else if (auto constructExpr = dyn_cast<CXXConstructExpr>(parent)) { + const CXXConstructorDecl * constructorDecl = constructExpr->getConstructor(); + for (unsigned i = 0; i < constructExpr->getNumArgs(); ++i) { + if (constructExpr->getArg(i) == stmt) { + return isPointerOrReferenceToConst(constructorDecl->getParamDecl(i)->getType()); + } + } + return false; // TODO ?? + } else if (auto operatorCallExpr = dyn_cast<CXXOperatorCallExpr>(parent)) { + const CXXMethodDecl* calleeMethodDecl = dyn_cast_or_null<CXXMethodDecl>(operatorCallExpr->getDirectCallee()); + if (calleeMethodDecl) { + // unary operator + if (calleeMethodDecl->getNumParams() == 0) { + // some classes like std::unique_ptr do not do a very good job with their operator-> which is always const + if (operatorCallExpr->getOperator() == OO_Arrow || operatorCallExpr->getOperator() == OO_Star) { + return checkIfCanBeConst(parent, cxxMethodDecl); + } + return calleeMethodDecl->isConst(); + } + // some classes like std::unique_ptr do not do a very good job with their operator[] which is always const + if (calleeMethodDecl->getNumParams() == 1 && operatorCallExpr->getArg(0) == stmt) { + if (operatorCallExpr->getOperator() == OO_Subscript) { + return false; + } + } + // binary operator + if (operatorCallExpr->getArg(0) == stmt) { + return calleeMethodDecl->isConst(); + } + unsigned const n = std::min( + operatorCallExpr->getNumArgs(), + calleeMethodDecl->getNumParams()); + for (unsigned i = 1; i < n; ++i) + if (operatorCallExpr->getArg(i) == stmt) { + return isPointerOrReferenceToConst(calleeMethodDecl->getParamDecl(i - 1)->getType()); + } + } else { + const Expr* callee = operatorCallExpr->getCallee()->IgnoreParenImpCasts(); + const DeclRefExpr* dr = dyn_cast<DeclRefExpr>(callee); + const FunctionDecl* calleeFunctionDecl = nullptr; + if (dr) { + calleeFunctionDecl = dyn_cast<FunctionDecl>(dr->getDecl()); + } + if (calleeFunctionDecl) { + for (unsigned i = 0; i < operatorCallExpr->getNumArgs(); ++i) { + if (operatorCallExpr->getArg(i) == stmt) { + return isPointerOrReferenceToConst(calleeFunctionDecl->getParamDecl(i)->getType()); + } + } + } + } + return false; // TODO ??? + } else if (auto callExpr = dyn_cast<CallExpr>(parent)) { + QualType functionType = callExpr->getCallee()->getType(); + if (functionType->isFunctionPointerType()) { + functionType = functionType->getPointeeType(); + } + if (const FunctionProtoType* prototype = functionType->getAs<FunctionProtoType>()) { + // TODO could do better + if (prototype->isVariadic()) { + return false; + } + if (callExpr->getCallee() == stmt) { + return true; + } + for (unsigned i = 0; i < callExpr->getNumArgs(); ++i) { + if (callExpr->getArg(i) == stmt) { + return isPointerOrReferenceToConst(prototype->getParamType(i)); + } + } + } + const FunctionDecl* calleeFunctionDecl = callExpr->getDirectCallee(); + if (calleeFunctionDecl) + { + if (auto memberCallExpr = dyn_cast<CXXMemberCallExpr>(parent)) { + const MemberExpr* memberExpr = dyn_cast<MemberExpr>(stmt); + if (memberExpr && memberCallExpr->getImplicitObjectArgument() == memberExpr->getBase()) + { + const CXXMethodDecl* calleeMethodDecl = dyn_cast<CXXMethodDecl>(calleeFunctionDecl); + // some classes like std::unique_ptr do not do a very good job with their get() which is always const + if (calleeMethodDecl->getIdentifier() && calleeMethodDecl->getName() == "get") { + return checkIfCanBeConst(parent, cxxMethodDecl); + } + // VclPtr<T>'s implicit conversion to T* + if (isa<CXXConversionDecl>(calleeMethodDecl)) { + if (loplugin::DeclCheck(calleeMethodDecl->getParent()).Class("OWeakObject").Namespace("cppu").GlobalNamespace()) + return false; + return checkIfCanBeConst(parent, cxxMethodDecl); + } + return calleeMethodDecl->isConst(); + } + } + // TODO could do better + if (calleeFunctionDecl->isVariadic()) { + return false; + } + if (callExpr->getCallee() == stmt) { + return true; + } + for (unsigned i = 0; i < callExpr->getNumArgs(); ++i) { + if (i >= calleeFunctionDecl->getNumParams()) // can happen in template code + return false; + if (callExpr->getArg(i) == stmt) { + return isPointerOrReferenceToConst(calleeFunctionDecl->getParamDecl(i)->getType()); + } + } + } + return false; // TODO ???? +// } else if (auto callExpr = dyn_cast<ObjCMessageExpr>(parent)) { +// if (callExpr->getInstanceReceiver() == stmt) { +// return true; +// } +// if (auto const method = callExpr->getMethodDecl()) { +// // TODO could do better +// if (method->isVariadic()) { +// return false; +// } +// assert(method->param_size() == callExpr->getNumArgs()); +// for (unsigned i = 0; i < callExpr->getNumArgs(); ++i) { +// if (callExpr->getArg(i) == stmt) { +// return isPointerOrReferenceToConst( +// method->param_begin()[i]->getType()); +// } +// } +// } +// return false; // TODO ???? + } else if (isa<CXXReinterpretCastExpr>(parent)) { + return false; + } else if (isa<ImplicitCastExpr>(parent)) { + return checkIfCanBeConst(parent, cxxMethodDecl); + } else if (isa<CXXStaticCastExpr>(parent)) { + return checkIfCanBeConst(parent, cxxMethodDecl); + } else if (isa<CXXDynamicCastExpr>(parent)) { + return checkIfCanBeConst(parent, cxxMethodDecl); + } else if (isa<CXXFunctionalCastExpr>(parent)) { + return checkIfCanBeConst(parent, cxxMethodDecl); + } else if (isa<CXXConstCastExpr>(parent)) { + return false; + } else if (isa<CStyleCastExpr>(parent)) { + return checkIfCanBeConst(parent, cxxMethodDecl); +// } else if (isa<CastExpr>(parent)) { // all other cast expression subtypes +// if (auto e = dyn_cast<ExplicitCastExpr>(parent)) { +// if (loplugin::TypeCheck(e->getTypeAsWritten()).Void()) { +// if (auto const sub = dyn_cast<DeclRefExpr>( +// e->getSubExpr()->IgnoreParenImpCasts())) +// { +// if (sub->getDecl() == cxxMethodDecl) { +// return false; +// } +// } +// } +// } +// return checkIfCanBeConst(parent, cxxMethodDecl); + } else if (isa<MemberExpr>(parent)) { + return checkIfCanBeConst(parent, cxxMethodDecl); + } else if (auto arraySubscriptExpr = dyn_cast<ArraySubscriptExpr>(parent)) { + if (arraySubscriptExpr->getIdx() == stmt) + return true; + return checkIfCanBeConst(parent, cxxMethodDecl); + } else if (isa<ParenExpr>(parent)) { + return checkIfCanBeConst(parent, cxxMethodDecl); + } else if (auto declStmt = dyn_cast<DeclStmt>(parent)) { + for (Decl const * decl : declStmt->decls()) + if (auto varDecl = dyn_cast<VarDecl>(decl)) { + if (varDecl->getInit() == stmt) { + auto tc = loplugin::TypeCheck(varDecl->getType()); + if (tc.LvalueReference() && !tc.LvalueReference().Const()) + return false; + if (tc.Pointer() && !tc.Pointer().Const()) + return false; + return true; + } + } + // fall through + } else if (isa<ReturnStmt>(parent)) { + return !isPointerOrReferenceToNonConst(cxxMethodDecl->getReturnType()); + } else if (isa<InitListExpr>(parent)) { + return false; // TODO could be improved + } else if (isa<IfStmt>(parent)) { + return true; + } else if (isa<WhileStmt>(parent)) { + return true; + } else if (isa<ForStmt>(parent)) { + return true; + } else if (isa<CompoundStmt>(parent)) { + return true; + } else if (isa<SwitchStmt>(parent)) { + return true; + } else if (isa<DoStmt>(parent)) { + return true; + } else if (isa<CXXDeleteExpr>(parent)) { + return false; +// } else if (isa<VAArgExpr>(parent)) { +// return false; + } else if (isa<CXXDependentScopeMemberExpr>(parent)) { + return false; + } else if (isa<MaterializeTemporaryExpr>(parent)) { + return checkIfCanBeConst(parent, cxxMethodDecl); + } else if (auto conditionalExpr = dyn_cast<ConditionalOperator>(parent)) { + if (conditionalExpr->getCond() == stmt) + return true; + return checkIfCanBeConst(parent, cxxMethodDecl); +// } else if (isa<UnaryExprOrTypeTraitExpr>(parent)) { +// return false; // ??? + } else if (isa<CXXNewExpr>(parent)) { +// for (auto pa : cxxNewExpr->placement_arguments()) +// if (pa == stmt) +// return false; + return true; // because the Stmt must be a parameter to the expression, probably an array length +// } else if (auto lambdaExpr = dyn_cast<LambdaExpr>(parent)) { +//// for (auto it = lambdaExpr->capture_begin(); it != lambdaExpr->capture_end(); ++it) +//// { +//// if (it->capturesVariable() && it->getCapturedVar() == cxxMethodDecl) +//// return it->getCaptureKind() != LCK_ByRef; +//// } +// return true; +// } else if (isa<CXXTypeidExpr>(parent)) { +// return true; + } else if (isa<ParenListExpr>(parent)) { + return true; + } else if (isa<CXXUnresolvedConstructExpr>(parent)) { + return false; +// } else if (isa<UnresolvedMemberExpr>(parent)) { +// return false; +// } else if (isa<PackExpansionExpr>(parent)) { +// return false; + } else if (isa<ExprWithCleanups>(parent)) { + return checkIfCanBeConst(parent, cxxMethodDecl); +// } else if (isa<CaseStmt>(parent)) { +// return true; +// } else if (isa<CXXPseudoDestructorExpr>(parent)) { +// return false; +// } else if (isa<CXXDependentScopeMemberExpr>(parent)) { +// return false; +// } else if (isa<ObjCIvarRefExpr>(parent)) { +// return checkIfCanBeConst(parent, cxxMethodDecl); + } else if (isa<CXXTemporaryObjectExpr>(parent)) { + return true; + } else if (isa<CXXBindTemporaryExpr>(parent)) { + return true; + } + if (parent) + parent->dump(); +// if (cxxMethodDecl) +// cxxMethodDecl->dump(); + report( + DiagnosticsEngine::Warning, + "oh dear, what can the matter be?", + parent->getBeginLoc()) + << parent->getSourceRange(); + return false; +} + +bool ConstMethod::isPointerOrReferenceToConst(const QualType& qt) { + auto const type = loplugin::TypeCheck(qt); + if (type.Pointer()) { + return bool(type.Pointer().Const()); + } else if (type.LvalueReference()) { + return bool(type.LvalueReference().Const()); + } + return false; +} + +bool ConstMethod::isPointerOrReferenceToNonConst(const QualType& qt) { + auto const type = loplugin::TypeCheck(qt); + if (type.Pointer()) { + return !bool(type.Pointer().Const()); + } else if (type.LvalueReference()) { + return !bool(type.LvalueReference().Const()); + } + return false; +} + +loplugin::Plugin::Registration< ConstMethod > X("constmethod", false); + +} + +/* vim:set shiftwidth=4 softtabstop=4 expandtab: */ |