Module:Parameter validation
To avoid major disruption and server load, any changes should be tested in the module's /sandbox or /testcases subpages, or in your own module sandbox. The tested changes can be added to this page in a single edit. Consider discussing changes on the talk page before implementing them.
This module helps you find problematic usages of a template's parameters. Compared to Module:Check for unknown parameters, you do not need to create long lists of parameters which should be categorised; it will automatically read them from the template's TemplateData. This means you don't need to define the parameter configuration in multiple places, which will inevitably lead to it being out of sync.
As well as that, it supports a few other types of errors, such as defining the same parameter multiple times, or using deprecated parameters (useful to do so you can replace usages before removing support for a parameter entirely).
Basic usage
Add this to the bottom of a template:
{{#invoke:Parameter validation|validateparams|module_options = Module:Parameter validation/default config}}
This will use the default config defined in Module:Parameter validation/default config. The module will begin to populate the following categories:
- Category:Pages using TEMPLATE_NAME with unknown parameters
- Category:Pages using TEMPLATE_NAME with deprecated parameters
- Category:Pages using TEMPLATE_NAME with duplicate parameters
For example, for {{Infobox station }}, it will populate:
- Category:Pages using infobox station with unknown parameters
- Category:Pages using infobox station with deprecated parameters
- Category:Pages using infobox station with duplicate parameters
Note that:
- Unknown parameters means a page calling the template used a parameter that does not exist in the template documentation's TemplateData section
- Deprecated parameters means a page calling the template used a parameter which, in the template's TemplateData, has the 'deprecated' option ticked
- Duplicate parameters means a page calling the template and set a value for a parameter and one or more of its aliases
Use in protected templates
Note that because templates' TemplateData code typically lives in unprotected /doc pages, protected templates that invoke this module can be made to incorrectly categorize pages by editors who do not have user rights sufficient to edit the template page itself.
Full documentation
This module is based on idea and original code of User:IKhitron.
The source of this module, along with the original documentation, can be found at he:Module:ParamValidator.
Editors can experiment in this module's sandbox (edit | diff) and testcases (create) pages.
Subpages of this module.
localutil={ empty=function(s) returns==nilortype(s)=='string'andmw.text.trim(s)=='' end , extract_options=function(frame,optionsPrefix) optionsPrefix=optionsPrefixor'options' localoptions,n,more={} ifframe.args['module_options']then localmodule_options=mw.loadData(frame.args['module_options']) iftype(module_options)~='table'thenreturn{}end localtitle=mw.title.getCurrentTitle() locallocal_ptions=module_options[title.namespace]ormodule_options[title.nsText]or{} fork,vinpairs(local_ptions)dooptions[k]=vend end repeat ok,more=pcall(mw.text.jsonDecode,frame.args[optionsPrefix..(nor'')]) ifokandtype(more)=='table'then fork,vinpairs(more)dooptions[k]=vend end n=(nor0)+1 untilnotok returnoptions end , build_namelist=function(template_name,sp) localres={template_name} ifspthen iftype(sp)=='string'thensp={sp}end for_,pinipairs(sp)dotable.insert(res,template_name..'/'..p)end end returnres end , table_empty=function(t)-- normally, test if next(t) is nil, but for some perverse reason, non-empty tables returned by loadData return nil... iftype(t)~='table'thenreturntrueend fora,binpairs(t)doreturnfalseend returntrue end , } localfunction_readTemplateData(templateName) localtitle=mw.title.makeTitle(0,templateName) localtemplateContent=titleandtitle.existsandtitle:getContent()-- template's raw content localcapture=templateContentandmw.ustring.match(templateContent,'<templatedata%s*>(.*)</templatedata%s*>')-- templatedata as text -- capture = capture and mw.ustring.gsub( capture, '"(%d+)"', tonumber ) -- convert "1": {} to 1: {}. frame.args uses numerical indexes for order-based params. localtrailingComma=captureandmw.ustring.find(capture,',%s*[%]%}]')-- look for ,] or ,} : jsonDecode allows it, but it's verbotten in json ifcaptureandnottrailingCommathenreturnpcall(mw.text.jsonDecode,capture)end returnfalse end localfunctionreadTemplateData(templateName) iftype(templateName)=='string'then templateName={templateName,templateName..'/'..docSubPage} end iftype(templateName)=="table"then for_,nameinipairs(templateName)do localtd,result=_readTemplateData(name) iftdthenreturnresultend end end returnnil end -- this is the function to be called by other modules. it expects the frame, and then an optional list of subpages, e.g. { "Documentation" }. -- if second parameter is nil, only template page will be searched for templatedata. functioncalculateViolations(frame,subpages) -- used for parameter type validy test. keyed by TD 'type' string. values are function(val) returning bool. localtype_validators={ ['number']=function(s)returnmw.language.getContentLanguage():parseFormattedNumber(s)end } functioncompatible(typ,val) localfunc=type_validators[typ] returntype(func)~='function'orutil.empty(val)orfunc(val) end localt_frame=frame:getParent() localt_args,template_name=t_frame.args,t_frame:getTitle() template_name=mw.ustring.gsub(template_name,'/sandbox','',1) localtd_source=util.build_namelist(template_name,subpages) ifframe.args['td_source']then table.insert(td_source,frame.args['td_source']) end localtemplatedata=readTemplateData(td_source) localtd_params=templatedataandtemplatedata.params localall_aliases,all_series={},{} ifnottd_paramsthenreturn{['no-templatedata']={['']=''}}end -- from this point on, we know templatedata is valid. localres={}-- before returning to caller, we'll prune empty tables -- allow for aliases forx,pinpairs(td_params)dofory,aliasinipairs(p.aliasesor{})do p['primary']=x td_params[x]=p all_aliases[alias]=p iftonumber(alias)thenall_aliases[tonumber(alias)]=pend endend -- handle undeclared and deprecated localalready_seen={} localseries=frame.args['series'] forp_name,valueinpairs(t_args)do localtp_param,noval,numeric,table_name=td_params[p_name]orall_aliases[p_name],util.empty(value),tonumber(p_name) localhasval=notnoval ifnottp_paramandseriesthen-- 2nd chance. check to see if series fors_name,pinpairs(td_params)do ifmw.ustring.match(p_name,'^'..s_name..'%d+'..'$')then -- mw.log('found p_name '.. p_name .. ' s_name:' .. s_name, ' p is:', p) debugging series support tp_param=p end-- don't bother breaking. td always correct. end end ifnottp_paramthen-- not in TD: this is called undeclared -- calculate the relevant table for this undeclared parameter, based on parameter and value types table_name= novalandnumericand'empty-undeclared-numeric'or novalandnotnumericand'empty-undeclared'or hasvalandnumericand'undeclared-numeric'or 'undeclared'-- tzvototi nishar. else-- in td: test for deprecation and mistype. if deprecated, no further tests table_name=tp_param.deprecatedandhasvaland'deprecated' ortp_param.deprecatedandnovaland'empty-deprecated' ornotcompatible(tp_param.type,value)and'incompatible' ornotseriesandalready_seen[tp_param]andhasvaland'duplicate' ifhasvalandtable_name~='duplicate'then already_seen[tp_param]=p_name end end -- report it. iftable_namethen res[table_name]=res[table_name]or{} iftable_name=='duplicate'then localprimary_param=tp_param['primary'] localprimaryData=res[table_name][primary_param] ifnotprimaryDatathen primaryData={} table.insert(primaryData,already_seen[tp_param]) end table.insert(primaryData,p_name) res[table_name][primary_param]=primaryData else res[table_name][p_name]=value end end end -- check for empty/missing parameters declared "required" forp_name,paraminpairs(td_params)do ifparam.requiredandutil.empty(t_args[p_name])then localis_alias for_,aliasinipairs(param.aliasesor{})dois_alias=is_aliasornotutil.empty(t_args[alias])end ifnotis_aliasthen res['empty-required']=res['empty-required']or{} res['empty-required'][p_name]='' end end end mw.logObject(res) returnres end -- wraps report in hidden frame functionwrapReport(report,template_name,options) mw.logObject(report) ifutil.empty(report)thenreturn''end localnaked=mw.title.new(template_name)['text'] naked=mw.ustring.gsub(naked,'Infobox','infobox',1) report=(options['wrapper-prefix']or"<div class = 'paramvalidator-wrapper'><span class='paramvalidator-error'>") ..report ..(options['wrapper-suffix']or"</span></div>") report=mw.ustring.gsub(report,'tname_naked',naked) report=mw.ustring.gsub(report,'templatename',template_name) returnreport end -- this is the "user" version, called with {{#invoke:}} returns a string, as defined by the options parameter functionvalidateParams(frame) localoptions,report,template_name=util.extract_options(frame),'',frame:getParent():getTitle() localignore=function(p_name) for_,patterninipairs(options['ignore']or{})do ifmw.ustring.match(p_name,'^'..pattern..'$')thenreturntrueend end returnfalse end localreplace_macros=function(error_type,s,param_names) functionconcat_and_escape(t,sep) sep=sepor', ' locals=table.concat(t,sep) return(mw.ustring.gsub(s,'%%','%%%%')) end ifsand(type(param_names)=='table')then localk_ar,kv_ar={},{} fork,vinpairs(param_names)do table.insert(k_ar,k) iftype(v)=='table'then v=table.concat(v,', ') end iferror_type=='duplicate'then table.insert(kv_ar,v) else table.insert(kv_ar,k..': '..v) end end s=mw.ustring.gsub(s,'paramname',concat_and_escape(k_ar)) s=mw.ustring.gsub(s,'paramandvalue',concat_and_escape(kv_ar,' AND ')) ifmw.getCurrentFrame():preprocess("{{REVISIONID}}")~=""then s=mw.ustring.gsub(s,"<div.*<%/div>","",1) end end returns end localreport_params=function(key,param_names) localres=replace_macros(key,options[key],param_names) res=frame:preprocess(resor'') report=report..(resor'') returnres end -- no option no work. ifutil.table_empty(options)thenreturn''end -- get the errors. localviolations=calculateViolations(frame,options['doc-subpage']) -- special request of bora: use skip_empty_numeric ifviolations['empty-undeclared-numeric']then fori=1,tonumber(options['skip-empty-numeric'])or0do violations['empty-undeclared-numeric'][i]=nil end end -- handle ignore list, and prune empty violations - in that order! localoffenders=0 forname,tabinpairs(violations)do -- remove ignored parameters from all violations forpnameinpairs(tab)doifignore(pname)thentab[pname]=nilendend -- prune empty violations ifutil.table_empty(tab)thenviolations[name]=nilend -- WORK IS DONE. report the errors. -- if report then count it. ifviolations[name]andreport_params(name,tab)thenoffenders=offenders+1end end ifoffenders>1thenreport_params('multiple')end ifoffenders~=0thenreport_params('any')end-- could have tested for empty( report ), but since we count them anyway... returnwrapReport(report,template_name,options) end return{ ['validateparams']=validateParams, ['calculateViolations']=calculateViolations, ['wrapReport']=wrapReport }