1 /* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2 /*
3 * This file is part of the LibreOffice project.
4 *
5 * This Source Code Form is subject to the terms of the Mozilla Public
6 * License, v. 2.0. If a copy of the MPL was not distributed with this
7 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
8 */
9
10 #include <cassert>
11 #include <string>
12 #include <set>
13 #include <iostream>
14
15 #include "plugin.hxx"
16
17 /**
18 Find parameters that have no name, i.e. they are unused and we're worked around the "unused parameter" warning.
19
20 Most of these can be removed.
21
22 TODO look for places where we are working around the warning by doing
23 (void) param1;
24 */
25 namespace {
26
27 class CheckUnusedParams: public loplugin::FilteringPlugin<CheckUnusedParams> {
28 public:
CheckUnusedParams(loplugin::InstantiationData const & data)29 explicit CheckUnusedParams(loplugin::InstantiationData const & data):
30 FilteringPlugin(data) {}
31 void run() override;
32 bool VisitFunctionDecl(FunctionDecl const *);
33 bool VisitUnaryOperator(UnaryOperator const *);
34 bool VisitInitListExpr(InitListExpr const *);
35 bool VisitCallExpr(CallExpr const *);
36 bool VisitBinaryOperator(BinaryOperator const *);
37 bool VisitCXXConstructExpr(CXXConstructExpr const *);
38 private:
39 void checkForFunctionDecl(Expr const *, bool bCheckOnly = false);
40 std::set<FunctionDecl const *> m_addressOfSet;
41 enum class PluginPhase { FindAddressOf, Warning };
42 PluginPhase m_phase;
43 };
44
run()45 void CheckUnusedParams::run()
46 {
47 StringRef fn(handler.getMainFileName());
48 if (loplugin::hasPathnamePrefix(fn, SRCDIR "/sal/"))
49 return;
50 // Taking pointer to function
51 if (loplugin::isSamePathname(fn, SRCDIR "/l10ntools/source/xmlparse.cxx"))
52 return;
53 // macro magic which declares something needed by an external library
54 if (loplugin::isSamePathname(fn, SRCDIR "/svl/source/misc/gridprinter.cxx"))
55 return;
56
57 // valid test/qa code
58 if (loplugin::hasPathnamePrefix(fn, SRCDIR "/compilerplugins/clang/test/"))
59 return;
60 if (loplugin::isSamePathname(fn, SRCDIR "/cppu/qa/test_reference.cxx"))
61 return;
62
63 // leave this alone for now
64 if (loplugin::hasPathnamePrefix(fn, SRCDIR "/libreofficekit/"))
65 return;
66 // this has a certain pattern to its code which appears to include lots of unused params
67 if (loplugin::hasPathnamePrefix(fn, SRCDIR "/xmloff/"))
68 return;
69 // I believe someone is busy working on this chunk of code
70 if (loplugin::isSamePathname(fn, SRCDIR "/sc/source/ui/docshell/dataprovider.cxx"))
71 return;
72 // I think erack is working on stuff here
73 if (loplugin::isSamePathname(fn, SRCDIR "/sc/source/filter/excel/xiformula.cxx"))
74 return;
75 // lots of callbacks here
76 if (loplugin::isSamePathname(fn, SRCDIR "/sc/source/filter/lotus/op.cxx"))
77 return;
78 // template magic
79 if (loplugin::isSamePathname(fn, SRCDIR "/sc/source/filter/html/htmlpars.cxx"))
80 return;
81
82 m_phase = PluginPhase::FindAddressOf;
83 TraverseDecl(compiler.getASTContext().getTranslationUnitDecl());
84 m_phase = PluginPhase::Warning;
85 TraverseDecl(compiler.getASTContext().getTranslationUnitDecl());
86 }
87
VisitUnaryOperator(UnaryOperator const * op)88 bool CheckUnusedParams::VisitUnaryOperator(UnaryOperator const * op) {
89 if (op->getOpcode() != UO_AddrOf) {
90 return true;
91 }
92 if (m_phase != PluginPhase::FindAddressOf)
93 return true;
94 checkForFunctionDecl(op->getSubExpr());
95 return true;
96 }
97
VisitBinaryOperator(BinaryOperator const * binaryOperator)98 bool CheckUnusedParams::VisitBinaryOperator(BinaryOperator const * binaryOperator) {
99 if (binaryOperator->getOpcode() != BO_Assign) {
100 return true;
101 }
102 if (m_phase != PluginPhase::FindAddressOf)
103 return true;
104 checkForFunctionDecl(binaryOperator->getRHS());
105 return true;
106 }
107
VisitCallExpr(CallExpr const * callExpr)108 bool CheckUnusedParams::VisitCallExpr(CallExpr const * callExpr) {
109 if (m_phase != PluginPhase::FindAddressOf)
110 return true;
111 for (auto arg : callExpr->arguments())
112 checkForFunctionDecl(arg);
113 return true;
114 }
115
VisitCXXConstructExpr(CXXConstructExpr const * constructExpr)116 bool CheckUnusedParams::VisitCXXConstructExpr(CXXConstructExpr const * constructExpr) {
117 if (m_phase != PluginPhase::FindAddressOf)
118 return true;
119 for (auto arg : constructExpr->arguments())
120 checkForFunctionDecl(arg);
121 return true;
122 }
123
VisitInitListExpr(InitListExpr const * initListExpr)124 bool CheckUnusedParams::VisitInitListExpr(InitListExpr const * initListExpr) {
125 if (m_phase != PluginPhase::FindAddressOf)
126 return true;
127 for (auto subStmt : *initListExpr)
128 checkForFunctionDecl(dyn_cast<Expr>(subStmt));
129 return true;
130 }
131
checkForFunctionDecl(Expr const * expr,bool bCheckOnly)132 void CheckUnusedParams::checkForFunctionDecl(Expr const * expr, bool bCheckOnly) {
133 auto e1 = expr->IgnoreParenCasts();
134 auto declRef = dyn_cast<DeclRefExpr>(e1);
135 if (!declRef)
136 return;
137 auto functionDecl = dyn_cast<FunctionDecl>(declRef->getDecl());
138 if (!functionDecl)
139 return;
140 if (bCheckOnly)
141 getParentStmt(expr)->dump();
142 else
143 m_addressOfSet.insert(functionDecl->getCanonicalDecl());
144 }
145
noFieldsInRecord(RecordType const * recordType)146 static int noFieldsInRecord(RecordType const * recordType) {
147 auto recordDecl = recordType->getDecl();
148 // if it's complicated, lets just assume it has fields
149 if (isa<ClassTemplateSpecializationDecl>(recordDecl))
150 return 1;
151 return std::distance(recordDecl->field_begin(), recordDecl->field_end());
152 }
startswith(const std::string & rStr,const char * pSubStr)153 static bool startswith(const std::string& rStr, const char* pSubStr) {
154 return rStr.compare(0, strlen(pSubStr), pSubStr) == 0;
155 }
endswith(const std::string & rStr,const char * pSubStr)156 static bool endswith(const std::string& rStr, const char* pSubStr) {
157 auto len = strlen(pSubStr);
158 if (len > rStr.size())
159 return false;
160 return rStr.compare(rStr.size() - len, rStr.size(), pSubStr) == 0;
161 }
162
VisitFunctionDecl(FunctionDecl const * decl)163 bool CheckUnusedParams::VisitFunctionDecl(FunctionDecl const * decl) {
164 if (m_phase != PluginPhase::Warning)
165 return true;
166 if (m_addressOfSet.find(decl->getCanonicalDecl()) != m_addressOfSet.end())
167 return true;
168 if (ignoreLocation(decl))
169 return true;
170 if (isInUnoIncludeFile(compiler.getSourceManager().getSpellingLoc(decl->getLocation())))
171 return true;
172
173 auto cxxMethodDecl = dyn_cast<CXXMethodDecl>(decl);
174 if (cxxMethodDecl) {
175 if (cxxMethodDecl->isVirtual())
176 return true;
177 auto cxxConstructorDecl = dyn_cast<CXXConstructorDecl>(cxxMethodDecl);
178 if (cxxConstructorDecl && cxxConstructorDecl->isCopyOrMoveConstructor())
179 return true;
180 }
181 if (!decl->isThisDeclarationADefinition())
182 return true;
183 if (decl->isFunctionTemplateSpecialization())
184 return true;
185 if (decl->isDeleted())
186 return true;
187 if (decl->getTemplatedKind() != clang::FunctionDecl::TK_NonTemplate)
188 return true;
189 if (decl->isOverloadedOperator())
190 return true;
191 if (decl->isExternC())
192 return true;
193
194 //TODO, filtering out any functions relating to class templates for now:
195 CXXRecordDecl const * r = dyn_cast<CXXRecordDecl>(decl->getDeclContext());
196 if (r != nullptr
197 && (r->getTemplateSpecializationKind() != TSK_Undeclared
198 || r->isDependentContext()))
199 {
200 return true;
201 }
202 FunctionDecl const * canon = decl->getCanonicalDecl();
203 std::string fqn = canon->getQualifiedNameAsString(); // because sometimes clang returns nonsense for the filename of canon
204 if (ignoreLocation(canon))
205 return true;
206 if (isInUnoIncludeFile(compiler.getSourceManager().getSpellingLoc(canon->getLocation())))
207 return true;
208 StringRef fn = getFilenameOfLocation(compiler.getSourceManager().getSpellingLoc(compat::getBeginLoc(canon)));
209 // Some backwards compat magic.
210 // TODO Can probably be removed, but need to do some checking
211 if (loplugin::isSamePathname(fn, SRCDIR "/include/sax/fshelper.hxx"))
212 return true;
213 // Platform-specific code
214 if (loplugin::isSamePathname(fn, SRCDIR "/include/svl/svdde.hxx"))
215 return true;
216 if (loplugin::isSamePathname(fn, SRCDIR "/include/vcl/svmain.hxx"))
217 return true;
218 // passing pointer to function
219 if (loplugin::isSamePathname(fn, SRCDIR "/include/vcl/BitmapReadAccess.hxx"))
220 return true;
221 if (loplugin::isSamePathname(fn, SRCDIR "/vcl/inc/unx/gtk/gtkobject.hxx"))
222 return true;
223 if (loplugin::isSamePathname(fn, SRCDIR "/vcl/inc/unx/gtk/gtksalframe.hxx"))
224 return true;
225 if (loplugin::isSamePathname(fn, SRCDIR "/vcl/inc/unx/gtk/gtkframe.hxx"))
226 return true;
227 if (loplugin::isSamePathname(fn, SRCDIR "/vcl/unx/gtk/fpicker/SalGtkFilePicker.hxx"))
228 return true;
229 if (loplugin::isSamePathname(fn, SRCDIR "/extensions/source/propctrlr/propertyeditor.hxx"))
230 return true;
231 if (loplugin::isSamePathname(fn, SRCDIR "/forms/source/solar/inc/navtoolbar.hxx"))
232 return true;
233 if (loplugin::isSamePathname(fn, SRCDIR "/hwpfilter/source/grammar.cxx"))
234 return true;
235 if (loplugin::isSamePathname(fn, SRCDIR "/hwpfilter/source/lexer.cxx"))
236 return true;
237 // marked with a TODO/FIXME
238 if (loplugin::isSamePathname(fn, SRCDIR "/vcl/inc/sallayout.hxx"))
239 return true;
240 if (loplugin::isSamePathname(fn, SRCDIR "/accessibility/inc/standard/vclxaccessiblelist.hxx"))
241 return true;
242 // these are "extern C" but clang doesn't seem to report that accurately
243 if (loplugin::isSamePathname(fn, SRCDIR "/sax/source/fastparser/fastparser.cxx"))
244 return true;
245 // these all follow the same pattern, seems a pity to break that
246 if (loplugin::isSamePathname(fn, SRCDIR "/include/vcl/graphicfilter.hxx"))
247 return true;
248 // looks like work in progress
249 if (loplugin::isSamePathname(fn, SRCDIR "/vcl/source/filter/ipdf/pdfdocument.cxx"))
250 return true;
251 // macro magic
252 if (loplugin::isSamePathname(fn, SRCDIR "/basctl/source/inc/basidesh.hxx"))
253 return true;
254 // template magic
255 if (loplugin::hasPathnamePrefix(fn, SRCDIR "/canvas/"))
256 return true;
257 if (loplugin::hasPathnamePrefix(fn, SRCDIR "/include/canvas/"))
258 return true;
259 if (loplugin::isSamePathname(fn, SRCDIR "/include/comphelper/unwrapargs.hxx"))
260 return true;
261 // this looks like vaguely useful code (ParseError) that I'm loathe to remove
262 if (loplugin::isSamePathname(fn, SRCDIR "/connectivity/source/inc/RowFunctionParser.hxx"))
263 return true;
264 if (loplugin::isSamePathname(fn, SRCDIR "/include/svx/EnhancedCustomShapeFunctionParser.hxx"))
265 return true;
266 // TODO marker parameter in constructor, should probably be using an enum
267 if (loplugin::isSamePathname(fn, SRCDIR "/framework/inc/uielement/uicommanddescription.hxx"))
268 return true;
269 if (loplugin::isSamePathname(fn, SRCDIR "/sd/source/ui/inc/SlideTransitionPane.hxx"))
270 return true;
271 if (loplugin::isSamePathname(fn, SRCDIR "/sd/source/ui/animations/CustomAnimationPane.hxx"))
272 return true;
273 if (loplugin::isSamePathname(fn, SRCDIR "/sd/source/ui/table/TableDesignPane.hxx"))
274 return true;
275 // debug stuff
276 if (loplugin::isSamePathname(fn, SRCDIR "/sc/source/core/data/column2.cxx"))
277 return true;
278 // weird stuff
279 if (loplugin::isSamePathname(fn, SRCDIR "/scaddins/source/analysis/analysishelper.hxx"))
280 return true;
281 // SFX_DECL_CHILDWINDOWCONTEXT macro stuff
282 if (loplugin::isSamePathname(fn, SRCDIR "/sd/source/ui/inc/NavigatorChildWindow.hxx"))
283 return true;
284 // TODO, need to remove this from the .sdi file too
285 if (loplugin::isSamePathname(fn, SRCDIR "/sd/source/ui/inc/SlideSorterViewShell.hxx"))
286 return true;
287 if (loplugin::isSamePathname(fn, SRCDIR "/sd/source/ui/inc/OutlineViewShell.hxx"))
288 return true;
289 // SFX_DECL_INTERFACE macro stuff
290 if (loplugin::isSamePathname(fn, SRCDIR "/sd/source/ui/inc/ViewShellBase.hxx"))
291 return true;
292 // debug stuff
293 if (loplugin::isSamePathname(fn, SRCDIR "/sd/source/filter/ppt/pptinanimations.hxx"))
294 return true;
295 // takes pointer to fn
296 if (loplugin::isSamePathname(fn, SRCDIR "/include/sfx2/shell.hxx"))
297 return true;
298 // TODO, need to remove this from the .sdi file too
299 if (fqn == "SfxObjectShell::StateView_Impl")
300 return true;
301 // SFX_DECL_CHILDWINDOW_WITHID macro
302 if (loplugin::isSamePathname(fn, SRCDIR "/include/sfx2/infobar.hxx"))
303 return true;
304 // this looks like vaguely useful code (ParseError) that I'm loathe to remove
305 if (loplugin::isSamePathname(fn, SRCDIR "/slideshow/source/inc/slideshowexceptions.hxx"))
306 return true;
307 // SFX_DECL_VIEWFACTORY macro
308 if (loplugin::isSamePathname(fn, SRCDIR "/starmath/inc/view.hxx"))
309 return true;
310 // debugging
311 if (fqn == "BrowseBox::DoShowCursor" || fqn == "BrowseBox::DoHideCursor")
312 return true;
313 // if I change this one, it then overrides a superclass virtual method
314 if (fqn == "GalleryBrowser2::KeyInput")
315 return true;
316 // takes pointer to function
317 if (fqn == "cmis::AuthProvider::onedriveAuthCodeFallback" || fqn == "cmis::AuthProvider::gdriveAuthCodeFallback")
318 return true;
319 if (fqn == "ooo_mount_operation_ask_password")
320 return true;
321 // TODO tricky to remove because of default params
322 if (fqn == "xmloff::OAttribute2Property::addBooleanProperty")
323 return true;
324 // taking pointer to function
325 if (fqn == "sw::DocumentContentOperationsManager::DeleteAndJoinWithRedlineImpl"
326 || fqn == "sw::DocumentContentOperationsManager::DeleteRangeImpl"
327 || fqn == "SwTableFormula::GetFormulaBoxes"
328 || fqn == "SwFEShell::Drag"
329 || fqn == "GetASCWriter" || fqn == "GetHTMLWriter" || fqn == "GetXMLWriter"
330 || fqn == "SwWrtShell::UpdateLayoutFrame" || fqn == "SwWrtShell::DefaultDrag"
331 || fqn == "SwWrtShell::DefaultEndDrag"
332 || startswith(fqn, "SwWW8ImplReader::Read_"))
333 return true;
334 // WIN32 only
335 if (fqn == "SwFntObj::GuessLeading")
336 return true;
337 // SFX_DECL_CHILDWINDOW_WITHID macro
338 if (fqn == "SwSpellDialogChildWindow::SwSpellDialogChildWindow"
339 || fqn == "SwFieldDlgWrapper::SwFieldDlgWrapper"
340 || fqn == "SwInputChild::SwInputChild")
341 return true;
342 // SFX_DECL_VIEWFACTORY macro
343 if (fqn == "SwSrcView::SwSrcView")
344 return true;
345 // Serves to disambiguate two very similar methods
346 if (fqn == "MSWordStyles::BuildGetSlot")
347 return true;
348 // TODO there are just too many default params to make this worth fixing right now
349 if (fqn == "ScDocument::CopyMultiRangeFromClip")
350 return true;
351 // TODO looks like this needs fixing?
352 if (fqn == "ScTable::ExtendPrintArea")
353 return true;
354 // there is a FIXME in the code
355 if (fqn == "ScRangeUtil::IsAbsTabArea")
356 return true;
357 // SFX_DECL_CHILDWINDOW_WITHID
358 if (fqn == "ScInputWindowWrapper::ScInputWindowWrapper"
359 || fqn == "sc::SearchResultsDlgWrapper::SearchResultsDlgWrapper")
360 return true;
361 // ExecMethod in .sdi file
362 if (fqn == "ScChartShell::ExecuteExportAsGraphic")
363 return true;
364 // bool marker parameter
365 if (fqn == "SvxIconReplacementDialog::SvxIconReplacementDialog")
366 return true;
367 // used as pointer to fn
368 if (endswith(fqn, "_createInstance"))
369 return true;
370 // callback
371 if (startswith(fqn, "SbRtl_"))
372 return true;
373 // takes pointer to fn
374 if (fqn == "migration::BasicMigration_create" || fqn == "migration::WordbookMigration_create"
375 || fqn == "comp_CBlankNode::_create" || fqn == "comp_CURI::_create"
376 || fqn == "comp_CLiteral::_create" || fqn == "CDocumentBuilder::_getInstance"
377 || fqn == "DOM::CDocumentBuilder::_getInstance"
378 || fqn == "xml_security::serial_number_adapter::create"
379 || fqn == "desktop::splash::create" || fqn == "ScannerManager_CreateInstance"
380 || fqn == "formula::FormulaOpCodeMapperObj::create"
381 || fqn == "(anonymous namespace)::createInstance"
382 || fqn == "x_error_handler"
383 || fqn == "warning_func"
384 || fqn == "error_func"
385 || fqn == "ScaDateAddIn_CreateInstance"
386 || fqn == "ScaPricingAddIn_CreateInstance"
387 || fqn == "(anonymous namespace)::PDFSigningPKCS7PasswordCallback"
388 || fqn == "ContextMenuEventLink"
389 || fqn == "DelayedCloseEventLink"
390 || fqn == "GDIMetaFile::ImplColMonoFnc"
391 || fqn == "vcl::getGlyph0"
392 || fqn == "vcl::getGlyph6"
393 || fqn == "vcl::getGlyph12"
394 || fqn == "setPasswordCallback"
395 || fqn == "VCLExceptionSignal_impl"
396 || fqn == "getFontTable"
397 || fqn == "textconversiondlgs::ChineseTranslation_UnoDialog::create"
398 || fqn == "pcr::DefaultHelpProvider::Create"
399 || fqn == "pcr::DefaultFormComponentInspectorModel::Create"
400 || fqn == "pcr::ObjectInspectorModel::Create"
401 || fqn == "GraphicExportFilter::GraphicExportFilter"
402 || fqn == "CertificateContainer::CertificateContainer"
403 || startswith(fqn, "ParseCSS1_")
404 )
405 return true;
406 // TODO
407 if (fqn == "FontSubsetInfo::CreateFontSubsetFromType1")
408 return true;
409 // used in template magic
410 if (fqn == "MtfRenderer::MtfRenderer" || fqn == "shell::sessioninstall::SyncDbusSessionHelper::SyncDbusSessionHelper"
411 || fqn == "dp_gui::LicenseDialog::LicenseDialog"
412 || fqn == "(anonymous namespace)::OGLTransitionFactoryImpl::OGLTransitionFactoryImpl")
413 return true;
414 // FIXME
415 if (fqn == "GtkSalDisplay::filterGdkEvent" || fqn == "SvXMLEmbeddedObjectHelper::ImplReadObject"
416 || fqn == "chart::CachedDataSequence::CachedDataSequence")
417 return true;
418 // used via macro
419 if (fqn == "framework::MediaTypeDetectionHelper::MediaTypeDetectionHelper"
420 || fqn == "framework::UriAbbreviation::UriAbbreviation"
421 || fqn == "framework::DispatchDisabler::DispatchDisabler"
422 || fqn == "framework::DispatchRecorderSupplier::DispatchRecorderSupplier")
423 return true;
424 // TODO Armin Le Grand is still working on this
425 if (fqn == "svx::frame::CreateDiagFrameBorderPrimitives"
426 || fqn == "svx::frame::CreateBorderPrimitives")
427 return true;
428 // marked with a TODO
429 if (fqn == "pcr::FormLinkDialog::getExistingRelation"
430 || fqn == "ooo::vba::DebugHelper::basicexception"
431 || fqn == "ScPrintFunc::DrawToDev")
432 return true;
433 // macros at work
434 if (fqn == "msfilter::lcl_PrintDigest")
435 return true;
436 // TODO something wrong here, the method that calls this (Normal::GenSlidingWindowFunction) cannot be correct
437 if (fqn == "sc::opencl::OpBase::Gen")
438 return true;
439 // Can't change this without conflicting with another constructor with the same signature
440 if (fqn == "XclExpSupbook::XclExpSupbook")
441 return true;
442 // ignore the LINK macros from include/tools/link.hxx
443 if (decl->getLocation().isMacroID())
444 return true;
445 // debug code in sw/
446 if (fqn == "lcl_dbg_out")
447 return true;
448
449 for( auto it = decl->param_begin(); it != decl->param_end(); ++it) {
450 auto param = *it;
451 if (param->hasAttr<UnusedAttr>())
452 continue;
453 if (!param->getName().empty())
454 continue;
455 // ignore params which are enum types with only a single enumerator, these are marker/tag types
456 auto paramType = param->getType();
457 if (paramType->isEnumeralType()) {
458 auto enumType = paramType->getAs<EnumType>();
459 int cnt = std::distance(enumType->getDecl()->enumerator_begin(), enumType->getDecl()->enumerator_end());
460 if (cnt == 1)
461 continue;
462 }
463 // ignore params which are a reference to a struct which has no fields.
464 // These are either
465 // (a) marker/tag types
466 // (b) selective "friend" access
467 if (paramType->isReferenceType()) {
468 auto referenceType = paramType->getAs<ReferenceType>();
469 if (referenceType->getPointeeType()->isRecordType()) {
470 auto recordType = referenceType->getPointeeType()->getAs<RecordType>();
471 if (noFieldsInRecord(recordType) == 0)
472 continue;
473 }
474 }
475 else if (paramType->isRecordType()) {
476 if (noFieldsInRecord(paramType->getAs<RecordType>()) == 0)
477 continue;
478 }
479 report( DiagnosticsEngine::Warning,
480 "unused param %0 in %1", compat::getBeginLoc(param))
481 << param->getSourceRange()
482 << param->getName()
483 << fqn;
484 if (canon != decl)
485 {
486 unsigned idx = param->getFunctionScopeIndex();
487 const ParmVarDecl* pOther = canon->getParamDecl(idx);
488 report( DiagnosticsEngine::Note, "declaration is here",
489 compat::getBeginLoc(pOther))
490 << pOther->getSourceRange();
491 }
492 }
493 return true;
494 }
495
496 loplugin::Plugin::Registration<CheckUnusedParams> X("checkunusedparams", false);
497
498 }
499
500 /* vim:set shiftwidth=4 softtabstop=4 expandtab: */
501