extend.html 41 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893
  1. <!DOCTYPE html>
  2. <!--
  3. Metrix++, Copyright 2009-2013, Metrix++ Project
  4. Link: http://metrixplusplus.sourceforge.net
  5. This file is part of Metrix++ Tool.
  6. Metrix++ is free software: you can redistribute it and/or modify
  7. it under the terms of the GNU General Public License as published by
  8. the Free Software Foundation, version 3 of the License.
  9. Metrix++ is distributed in the hope that it will be useful,
  10. but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. GNU General Public License for more details.
  13. You should have received a copy of the GNU General Public License
  14. along with Metrix++. If not, see <http://www.gnu.org/licenses/>.
  15. -->
  16. <html lang="en">
  17. <head>
  18. <meta charset="utf-8">
  19. <title>Metrix++ Project</title>
  20. <meta name="viewport" content="width=device-width, initial-scale=1.0">
  21. <meta name="description" content="">
  22. <meta name="author" content="">
  23. <!-- Le styles -->
  24. <!--
  25. <link href="../../style.css" rel="stylesheet">
  26. -->
  27. <link href="assets/css/bootstrap.css" rel="stylesheet">
  28. <link href="assets/css/bootstrap-responsive.css" rel="stylesheet">
  29. <link href="assets/css/docs.css" rel="stylesheet">
  30. <link href="assets/js/google-code-prettify/prettify.css" rel="stylesheet">
  31. <!-- Le HTML5 shim, for IE6-8 support of HTML5 elements -->
  32. <!--[if lt IE 9]>
  33. <script src="assets/js/html5shiv.js"></script>
  34. <![endif]-->
  35. <!-- Le fav and touch icons -->
  36. <link rel="apple-touch-icon-precomposed" sizes="144x144" href="assets/ico/apple-touch-icon-144-precomposed.png">
  37. <link rel="apple-touch-icon-precomposed" sizes="114x114" href="assets/ico/apple-touch-icon-114-precomposed.png">
  38. <link rel="apple-touch-icon-precomposed" sizes="72x72" href="assets/ico/apple-touch-icon-72-precomposed.png">
  39. <link rel="apple-touch-icon-precomposed" href="assets/ico/apple-touch-icon-57-precomposed.png">
  40. <link rel="shortcut icon" href="assets/ico/favicon.png">
  41. <!--
  42. <script type="text/javascript">
  43. var _gaq = _gaq || [];
  44. _gaq.push(['_setAccount', 'UA-146052-10']);
  45. _gaq.push(['_trackPageview']);
  46. (function() {
  47. var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
  48. ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
  49. var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
  50. })();
  51. </script>
  52. -->
  53. </head>
  54. <body data-spy="scroll" data-target=".bs-docs-sidebar">
  55. <!-- Navbar
  56. ================================================== -->
  57. <div class="navbar navbar-link navbar-fixed-top">
  58. <div class="navbar-inner">
  59. <div class="container">
  60. <button type="button" class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
  61. <span class="icon-bar"></span>
  62. <span class="icon-bar"></span>
  63. <span class="icon-bar"></span>
  64. </button>
  65. <a class="brand" href="./index.html">Metrix++</a>
  66. <div class="nav-collapse collapse">
  67. <ul class="nav">
  68. <li class="">
  69. <a href="./index.html">Home</a>
  70. </li>
  71. <li class="">
  72. <a href="./workflow.html">Workflow</a>
  73. </li>
  74. <li class="">
  75. <a href="./extend.html">Create plugin</a>
  76. </li>
  77. </ul>
  78. </div>
  79. </div>
  80. </div>
  81. </div>
  82. <!-- Subhead
  83. ================================================== -->
  84. <header class="jumbotron" id="overview">
  85. <div class="container">
  86. <div class="row">
  87. <div class="span3"></div>
  88. <div class="span9">
  89. <h5 class="text-right">Management of source code quality is possible.</h5>
  90. <p class="text-right">
  91. <a href="https://sourceforge.net/projects/metrixplusplus/files/latest/download"
  92. ><button type="button"class="btn btn-danger">Download</button></a>
  93. <!--
  94. <button type="button"class="btn btn-warning">Donate</button>
  95. -->
  96. </p>
  97. </div>
  98. </div>
  99. </div>
  100. </header>
  101. <div class="container"><div class="row">
  102. <!-- Docs nav
  103. ================================================== -->
  104. <div class="span3 bs-docs-sidebar">
  105. <ul class="nav nav-list bs-docs-sidenav">
  106. <li><a href="#metric_plugin"><i class="icon-chevron-right"></i> Metric plugin</a></li>
  107. <li><a href="#tool_plugin"><i class="icon-chevron-right"></i> Post-analysis tool</a></li>
  108. <li><a href="#language_plugin"><i class="icon-chevron-right"></i> Language parser</a></li>
  109. </ul>
  110. </div>
  111. <!-- Sections
  112. ================================================== -->
  113. <div class="span9">
  114. <div class="page-header">
  115. <h1>Create plugin</h1>
  116. </div>
  117. <p>There are 3 types of plugins considered in this chapter:</p>
  118. <ul>
  119. <li>Metric plugin</li>
  120. <li>Language parser</li>
  121. <li>Post-processing / Analysis tool</li>
  122. </ul>
  123. <p>Tutorial for metric plugin is generic at the beginning and large portion of this is applied to
  124. all other plugins. You need to know python and python regular expressions library to write Metrix++ extensions.</p>
  125. <section id="metric_plugin">
  126. <h2>Metric plugin</h2>
  127. <p>The tutorial will explain how to create a plugin to count magic numbers in source code.
  128. It will be relatively simple at first and will be extended with additional configuration
  129. options and smarter counting logic.</p>
  130. <h4>Create placeholder for new plugin</h4>
  131. <ol>
  132. <li>All plugins are loaded by Metrix++ from standard places within the tool installation directory and
  133. from custom places specified in the METRIXPLUSPLUS_PATH environment variable.
  134. METRIXPLUSPLUS_PATH has got the same format as system PATH environment variable.
  135. So, the first step in plugin development is to set the METRIXPLUSPLUS_PATH to point out to
  136. the directory (or directories) where plugin is located.</li>
  137. <li>Create new python package 'myext', python lib 'magic.py' and 'magic.ini' file.</li>
  138. <pre>
  139. + working_directory (set in METRIXPLUSPLUS_PATH variable)
  140. \--+ myext
  141. \--- __init__.py
  142. \--- magic.py
  143. \--- magic.ini
  144. </pre>
  145. <li>__init__.py is empty file to make myext considered by python as a package.</li>
  146. <li>Edit magic.py to have the following content:
  147. <pre class="prettyprint linenums">
  148. import mpp.api
  149. class Plugin(mpp.api.Plugin):
  150. def initialize(self):
  151. print "Hello world"
  152. </pre>
  153. mpp.api package include Metrix++ API classes. mpp.api.Plugin is the base class, which can be loaded
  154. by Metrix++ engine and does nothing by default. In the code sample above it is extended to print
  155. "Hello world" on initialization.</li>
  156. <li>Edit magic.ini to have the following content:
  157. <pre class="prettyprint linenums">
  158. [Plugin]
  159. version: 1.0
  160. package: myext
  161. module: magic
  162. class: Plugin
  163. depends: None
  164. actions: collect
  165. enabled: True
  166. </pre>
  167. This file is a manifest for Metrix++ plugin loader. The fields in Plugin section are:
  168. <dl class="dl-horizontal">
  169. <dt>version</dt>
  170. <dd>a string representing the version, step up it every time when behaviour of a plugin
  171. or backward compatibility in api or data scope is changed</dd>
  172. <dt>package</dt>
  173. <dd>python package name where to load from</dd>
  174. <dt>module</dt>
  175. <dd>python module name (filename of *.py file) to load</dd>
  176. <dt>class</dt>
  177. <dd>name of a plugin class to instanciate</dd>
  178. <dt>depends</dt>
  179. <dd>list of plugin names to load, if it this plugin is loaded</dd>
  180. <dt>actions</dt>
  181. <dd>list of Metrix++ actions affected by this plugin</dd>
  182. <dt>enabled</dt>
  183. <dd>True or False, working status of a plugin</dd>
  184. </dl>
  185. </li>
  186. <li>Now run Metrix++ to see how this new plugin works:</li>
  187. <pre>&gt; python "/path/to/metrix++.py" collect</pre>
  188. <pre>Hello world</pre>
  189. </ol>
  190. <h4>Toogle option for the plugin</h4>
  191. <ol>
  192. <li>It is recommended to follow the convention for all plugins: 'run only if enabled'.
  193. So, let's extend the magic.py file to make it configurable
  194. <pre class="prettyprint linenums">
  195. import mpp.api
  196. class Plugin(mpp.api.Plugin,
  197. # make this instance configurable...
  198. mpp.api.IConfigurable):
  199. # ... and implement 2 interfaces
  200. def declare_configuration(self, parser):
  201. parser.add_option("--myext.magic.numbers", "--mmn",
  202. action="store_true", default=False,
  203. help="Enables collection of magic numbers metric [default: %default]")
  204. def configure(self, options):
  205. self.is_active_numbers = options.__dict__['myext.magic.numbers']
  206. def initialize(self):
  207. # use configuration option here
  208. if self.is_active_numbers == True:
  209. print "Hello world"
  210. </pre>
  211. parser argument is an instance of optparse.OptionParser class. It has got an extension to
  212. accept multiple options of the same argument. Check std.tools.limit to see how to declare multiopt options, if you need.</li>
  213. <li>Now run Metrix++ to see how this works:</li>
  214. <pre>&gt; python "/path/to/metrix++.py" collect --myext.magic.numbers</pre>
  215. <pre>Hello world</pre>
  216. </ol>
  217. <h4>Subscribe to notifications from parent plugins (or code parsers)</h4>
  218. <ol>
  219. <li>Every plugin works in a callback functions called by parent plugins.
  220. Callback receives a reference to parent plugin, data object where to store metrics data,
  221. and a flag indicating if there are changes in file or parent's settings since the previous collection.</li>
  222. <pre class="prettyprint linenums">
  223. import mpp.api
  224. class Plugin(mpp.api.Plugin,
  225. mpp.api.IConfigurable,
  226. # declare that it can subscribe on notifications
  227. mpp.api.Child):
  228. def declare_configuration(self, parser):
  229. parser.add_option("--myext.magic.numbers", "--mmn",
  230. action="store_true", default=False,
  231. help="Enables collection of magic numbers metric [default: %default]")
  232. def configure(self, options):
  233. self.is_active_numbers = options.__dict__['myext.magic.numbers']
  234. def initialize(self):
  235. if self.is_active_numbers == True:
  236. # subscribe to notifications from all code parsers
  237. self.subscribe_by_parents_interface(mpp.api.ICode, 'callback')
  238. # parents (code parsers) will call the callback declared
  239. def callback(self, parent, data, is_updated):
  240. print parent.get_name(), data.get_path(), is_updated
  241. </pre>
  242. <li>Now run Metrix++ to see how this works. Try to do iterative scans (--db-file-prev option) to see how the
  243. state of arguments is changed</li>
  244. <pre>&gt; python "/path/to/metrix++.py" collect --myext.magic.numbers</pre>
  245. <pre>std.code.cpp ./test.cpp True</pre>
  246. </ol>
  247. <h4>Implement simple metric based on regular expression pattern</h4>
  248. <ol>
  249. <li>Callback may execute counting, searcing and additional parsing and store results, using data argument.
  250. 'data' argument is an instance of mpp.api.FileData class.
  251. However, most metrics can be implemented
  252. simplier, if mpp.api.MetricPluginMixin routines are used. MetricPluginMixin implements
  253. declarative style for metrics based on searches by regular expression. It
  254. cares about initialisation of database fields and properties.
  255. It implements default callback which counts number of matches by regular expression for all
  256. active declared metrics. So, let's utilise that:
  257. </li>
  258. <pre class="prettyprint linenums">
  259. import mpp.api
  260. import re
  261. class Plugin(mpp.api.Plugin,
  262. mpp.api.IConfigurable,
  263. mpp.api.Child,
  264. # reuse by inheriting standard metric facilities
  265. mpp.api.MetricPluginMixin):
  266. def declare_configuration(self, parser):
  267. parser.add_option("--myext.magic.numbers", "--mmn",
  268. action="store_true", default=False,
  269. help="Enables collection of magic numbers metric [default: %default]")
  270. def configure(self, options):
  271. self.is_active_numbers = options.__dict__['myext.magic.numbers']
  272. def initialize(self):
  273. # declare metric rules
  274. self.declare_metric(
  275. self.is_active_numbers, # to count if active in callback
  276. self.Field('numbers', int), # field name and type in the database
  277. re.compile(r'''\b[0-9]+\b'''), # pattern to search
  278. marker_type_mask=mpp.api.Marker.T.CODE, # search in code
  279. region_type_mask=mpp.api.Region.T.ANY) # search in all types of regions
  280. # use superclass facilities to initialize everything from declared fields
  281. super(Plugin, self).initialize(fields=self.get_fields())
  282. # subscribe to all code parsers if at least one metric is active
  283. if self.is_active() == True:
  284. self.subscribe_by_parents_interface(mpp.api.ICode)
  285. </pre>
  286. <li>Now run Metrix++ to count numbers in code files.</li>
  287. <pre>&gt; python "/path/to/metrix++.py" collect --myext.magic.numbers</pre>
  288. <li>Now view the results. At this stage it is fully working simple metric.</li>
  289. <pre>&gt; python "/path/to/metrix++.py" view</pre>
  290. <pre>
  291. :: info: Overall metrics for 'myext.magic:numbers' metric
  292. Average : 2.75
  293. Minimum : 0
  294. Maximum : 7
  295. Total : 11.0
  296. Distribution : 4 regions in total (including 0 suppressed)
  297. Metric value : Ratio : R-sum : Number of regions
  298. 0 : 0.250 : 0.250 : 1 |||||||||||||||||||||||||
  299. 1 : 0.250 : 0.500 : 1 |||||||||||||||||||||||||
  300. 3 : 0.250 : 0.750 : 1 |||||||||||||||||||||||||
  301. 7 : 0.250 : 1.000 : 1 |||||||||||||||||||||||||
  302. :: info: Directory content:
  303. Directory : .
  304. </pre>
  305. </ol>
  306. <h4>Extend regular expression incremental counting by smarter logic</h4>
  307. <ol>
  308. <li>At this stage the metric counts every number in source code.
  309. However, we indent to spot only 'magic' numbers. Declared constant
  310. is not a magic number, so it is better to exclude constants from counting.
  311. It is easy to change default counter behaviour by implementing
  312. a function with name '_&lt;metric_name&gt;_count'. </li>
  313. <pre class="prettyprint linenums">
  314. import mpp.api
  315. import re
  316. class Plugin(mpp.api.Plugin,
  317. mpp.api.IConfigurable,
  318. mpp.api.Child,
  319. mpp.api.MetricPluginMixin):
  320. def declare_configuration(self, parser):
  321. parser.add_option("--myext.magic.numbers", "--mmn",
  322. action="store_true", default=False,
  323. help="Enables collection of magic numbers metric [default: %default]")
  324. def configure(self, options):
  325. self.is_active_numbers = options.__dict__['myext.magic.numbers']
  326. def initialize(self):
  327. # improve pattern to find declarations of constants
  328. pattern_to_search = re.compile(
  329. r'''((const(\s+[_a-zA-Z][_a-zA-Z0-9]*)+\s*[=]\s*)[-+]?[0-9]+\b)|(\b[0-9]+\b)''')
  330. self.declare_metric(self.is_active_numbers,
  331. self.Field('numbers', int),
  332. # give a pair of pattern + custom counter logic class
  333. (pattern_to_search, self.NumbersCounter),
  334. marker_type_mask=mpp.api.Marker.T.CODE,
  335. region_type_mask=mpp.api.Region.T.ANY)
  336. super(Plugin, self).initialize(fields=self.get_fields())
  337. if self.is_active() == True:
  338. self.subscribe_by_parents_interface(mpp.api.ICode)
  339. # implement custom counter behavior:
  340. # increments counter by 1 only if single number spotted,
  341. # but not declaration of a constant
  342. class NumbersCounter(mpp.api.MetricPluginMixin.IterIncrementCounter):
  343. def increment(self, match):
  344. if match.group(0).startswith('const'):
  345. return 0
  346. return 1
  347. </pre>
  348. <li>Initialy counter is initialized by zero, but it is possible to
  349. change it as well by implementing a function with name '_&lt;metric_name&gt;_count_initialize'.
  350. Plugin we are implementing does not require this.</li>
  351. <li>Now run Metrix++ to collect and view the results.</li>
  352. <pre>&gt; python "/path/to/metrix++.py" collect --myext.magic.numbers</pre>
  353. <pre>&gt; python "/path/to/metrix++.py" view</pre>
  354. </ol>
  355. <h4>Language specific regular expressions</h4>
  356. <ol>
  357. <li>In the previous step we added matching of constants assuming that identifiers
  358. may have symbols '_', 'a-z', 'A-Z' and '0-9'. It is true for C++ but it is not complete for Java.
  359. Java identifier may have '$' symbol in the identifier. So, let's add language specific pattern
  360. in the declaration of the metric:</li>
  361. <pre class="prettyprint linenums">
  362. import mpp.api
  363. import re
  364. class Plugin(mpp.api.Plugin,
  365. mpp.api.IConfigurable,
  366. mpp.api.Child,
  367. mpp.api.MetricPluginMixin):
  368. def declare_configuration(self, parser):
  369. parser.add_option("--myext.magic.numbers", "--mmn",
  370. action="store_true", default=False,
  371. help="Enables collection of magic numbers metric [default: %default]")
  372. def configure(self, options):
  373. self.is_active_numbers = options.__dict__['myext.magic.numbers']
  374. def initialize(self):
  375. # specialized pattern for java
  376. pattern_to_search_java = re.compile(
  377. r'''((const(\s+[_$a-zA-Z][_$a-zA-Z0-9]*)+\s*[=]\s*)[-+]?[0-9]+\b)|(\b[0-9]+\b)''')
  378. # pattern for C++ and C# languages
  379. pattern_to_search_cpp_cs = re.compile(
  380. r'''((const(\s+[_a-zA-Z][_a-zA-Z0-9]*)+\s*[=]\s*)[-+]?[0-9]+\b)|(\b[0-9]+\b)''')
  381. # pattern for all other languages
  382. pattern_to_search = re.compile(
  383. r'''\b[0-9]+\b''')
  384. self.declare_metric(self.is_active_numbers,
  385. self.Field('numbers', int),
  386. # dictionary of pairs instead of a single pair
  387. {
  388. 'std.code.java': (pattern_to_search_java, self.NumbersCounter),
  389. 'std.code.cpp': (pattern_to_search_cpp_cs, self.NumbersCounter),
  390. 'std.code.cs': (pattern_to_search_cpp_cs, self.NumbersCounter),
  391. '*': pattern_to_search
  392. },
  393. marker_type_mask=mpp.api.Marker.T.CODE,
  394. region_type_mask=mpp.api.Region.T.ANY)
  395. super(Plugin, self).initialize(fields=self.get_fields())
  396. if self.is_active() == True:
  397. self.subscribe_by_parents_interface(mpp.api.ICode)
  398. class NumbersCounter(mpp.api.MetricPluginMixin.IterIncrementCounter):
  399. def increment(self, match):
  400. if match.group(0).startswith('const'):
  401. return 0
  402. return 1
  403. </pre>
  404. <li>Keys in the dictionary of patterns are names of parent plugins (references to code parsers).
  405. The key '*' refers to any parser.</li>
  406. <li>Now run Metrix++ to collect and view the results.</li>
  407. <pre>&gt; python "/path/to/metrix++.py" collect --myext.magic.numbers</pre>
  408. <pre>&gt; python "/path/to/metrix++.py" view</pre>
  409. </ol>
  410. <h4>Store only non-zero metric values</h4>
  411. <ol>
  412. <li>Most functions have the metric, which we are implemneting, equal to zero.
  413. However, we are interested in finding code blocks having this metric greater than zero.
  414. Zeros consumes the space in the data file. So, we can optimise the size of a data file,
  415. if we exclude zero metric values. Let's declare this behavior for the metric.</li>
  416. <pre class="prettyprint linenums">
  417. import mpp.api
  418. import re
  419. class Plugin(mpp.api.Plugin,
  420. mpp.api.IConfigurable,
  421. mpp.api.Child,
  422. mpp.api.MetricPluginMixin):
  423. def declare_configuration(self, parser):
  424. parser.add_option("--myext.magic.numbers", "--mmn",
  425. action="store_true", default=False,
  426. help="Enables collection of magic numbers metric [default: %default]")
  427. def configure(self, options):
  428. self.is_active_numbers = options.__dict__['myext.magic.numbers']
  429. def initialize(self):
  430. pattern_to_search_java = re.compile(
  431. r'''((const(\s+[_$a-zA-Z][_$a-zA-Z0-9]*)+\s*[=]\s*)[-+]?[0-9]+\b)|(\b[0-9]+\b)''')
  432. pattern_to_search_cpp_cs = re.compile(
  433. r'''((const(\s+[_a-zA-Z][_a-zA-Z0-9]*)+\s*[=]\s*)[-+]?[0-9]+\b)|(\b[0-9]+\b)''')
  434. pattern_to_search = re.compile(
  435. r'''\b[0-9]+\b''')
  436. self.declare_metric(self.is_active_numbers,
  437. self.Field('numbers', int,
  438. # optimize the size of datafile:
  439. # store only non-zero results
  440. non_zero=True),
  441. {
  442. 'std.code.java': (pattern_to_search_java, self.NumbersCounter),
  443. 'std.code.cpp': (pattern_to_search_cpp_cs, self.NumbersCounter),
  444. 'std.code.cs': (pattern_to_search_cpp_cs, self.NumbersCounter),
  445. '*': pattern_to_search
  446. },
  447. marker_type_mask=mpp.api.Marker.T.CODE,
  448. region_type_mask=mpp.api.Region.T.ANY)
  449. super(Plugin, self).initialize(fields=self.get_fields())
  450. if self.is_active() == True:
  451. self.subscribe_by_parents_interface(mpp.api.ICode)
  452. class NumbersCounter(mpp.api.MetricPluginMixin.IterIncrementCounter):
  453. def increment(self, match):
  454. if match.group(0).startswith('const'):
  455. return 0
  456. return 1
  457. </pre>
  458. <li>Now run Metrix++ to collect and view the results.</li>
  459. <pre>&gt; python "/path/to/metrix++.py" collect --myext.magic.numbers</pre>
  460. <pre>&gt; python "/path/to/metrix++.py" view</pre>
  461. </ol>
  462. <h4>Additional per metric configuration options</h4>
  463. <ol>
  464. <li>It is typical that most numbers counted by the metric are equal to 0, -1 or 1.
  465. They are not necessary magic numbers. 0 or 1 are typical variable initializers.
  466. -1 is a typical negative return code. So, let's implement simplified version of the metric,
  467. which does not count 0, -1 and 1, if the specific new option is set.</li>
  468. <pre class="prettyprint linenums">
  469. import mpp.api
  470. import re
  471. class Plugin(mpp.api.Plugin,
  472. mpp.api.IConfigurable,
  473. mpp.api.Child,
  474. mpp.api.MetricPluginMixin):
  475. def declare_configuration(self, parser):
  476. parser.add_option("--myext.magic.numbers", "--mmn",
  477. action="store_true", default=False,
  478. help="Enables collection of magic numbers metric [default: %default]")
  479. # Add new option
  480. parser.add_option("--myext.magic.numbers.simplier", "--mmns",
  481. action="store_true", default=False,
  482. help="Is set, 0, -1 and 1 numbers are not counted [default: %default]")
  483. def configure(self, options):
  484. self.is_active_numbers = options.__dict__['myext.magic.numbers']
  485. # remember the option here
  486. self.is_active_numbers_simplier = options.__dict__['myext.magic.numbers.simplier']
  487. def initialize(self):
  488. pattern_to_search_java = re.compile(
  489. r'''((const(\s+[_$a-zA-Z][_$a-zA-Z0-9]*)+\s*[=]\s*)[-+]?[0-9]+\b)|(\b[0-9]+\b)''')
  490. pattern_to_search_cpp_cs = re.compile(
  491. r'''((const(\s+[_a-zA-Z][_a-zA-Z0-9]*)+\s*[=]\s*)[-+]?[0-9]+\b)|(\b[0-9]+\b)''')
  492. pattern_to_search = re.compile(
  493. r'''\b[0-9]+\b''')
  494. self.declare_metric(self.is_active_numbers,
  495. self.Field('numbers', int,
  496. non_zero=True),
  497. {
  498. 'std.code.java': (pattern_to_search_java, self.NumbersCounter),
  499. 'std.code.cpp': (pattern_to_search_cpp_cs, self.NumbersCounter),
  500. 'std.code.cs': (pattern_to_search_cpp_cs, self.NumbersCounter),
  501. '*': pattern_to_search
  502. },
  503. marker_type_mask=mpp.api.Marker.T.CODE,
  504. region_type_mask=mpp.api.Region.T.ANY)
  505. super(Plugin, self).initialize(fields=self.get_fields(),
  506. # remember option settings in data file properties
  507. # in order to detect changes in settings on iterative re-run
  508. properties=[self.Property('number.simplier', self.is_active_numbers_simplier)])
  509. if self.is_active() == True:
  510. self.subscribe_by_parents_interface(mpp.api.ICode)
  511. class NumbersCounter(mpp.api.MetricPluginMixin.IterIncrementCounter):
  512. def increment(self, match):
  513. if (match.group(0).startswith('const') or
  514. (self.plugin.is_active_numbers_simplier == True and
  515. match.group(0) in ['0', '1', '-1', '+1'])):
  516. return 0
  517. return 1
  518. </pre>
  519. <li>Now run Metrix++ to collect and view the results.</li>
  520. <pre>&gt; python "/path/to/metrix++.py" collect --myext.magic.numbers</pre>
  521. <pre>&gt; python "/path/to/metrix++.py" view</pre>
  522. <pre>
  523. :: info: Overall metrics for 'myext.magic:numbers' metric
  524. Average : 2.5 (excluding zero metric values)
  525. Minimum : 2
  526. Maximum : 3
  527. Total : 5.0
  528. Distribution : 2 regions in total (including 0 suppressed)
  529. Metric value : Ratio : R-sum : Number of regions
  530. 2 : 0.500 : 0.500 : 1 ||||||||||||||||||||||||||||||||||||||||||||||||||
  531. 3 : 0.500 : 1.000 : 1 ||||||||||||||||||||||||||||||||||||||||||||||||||
  532. :: info: Directory content:
  533. Directory : .
  534. </pre>
  535. </ol>
  536. <h4>Summary</h4>
  537. <p>We have finished with the tutorial. The tutorial explained how to implement simple and advanced metric plugins.
  538. We used built-in Metrix++ base classes. If you need to more advanced plugin capabilities,
  539. override in your plugin class functions inherited from mpp.api base classes. Check code of standard plugins
  540. to learn more techniques.</p>
  541. <p></p>
  542. </section>
  543. <section id="tool_plugin">
  544. <h2>Analysis tool plugin</h2>
  545. <p>This tutorial will explain how to build custom Metrix++ command, which is bound to custom post-analysis tool.
  546. We will implement the tool, which identifies all new and changed regions and counts number of added lines.
  547. We skip calculating number of deleted lines, but it is easy to extend from what we get finally in the tutorial.</p>
  548. <h4>New Metrix++ command / action</h4>
  549. <ol>
  550. <li>As in the tutorial for metric plugin, set the environment and
  551. create new python package 'myext', python lib 'compare.py' and 'compare.ini' file.</li>
  552. <pre>
  553. + working_directory (set in METRIXPLUSPLUS_PATH variable)
  554. \--+ myext
  555. \--- __init__.py
  556. \--- compare.py
  557. \--- compare.ini
  558. </pre>
  559. <li>__init__.py is empty file to make myext considered by python as a package.</li>
  560. <li>Edit compare.py to have the following content:
  561. <pre class="prettyprint linenums">
  562. import mpp.api
  563. class Plugin(mpp.api.Plugin, mpp.api.IRunable):
  564. def run(self, args):
  565. print args
  566. return 0
  567. </pre>
  568. Inheritance from mpp.api.IRunable declares that the plugin is runable and requires implementation of 'run' interface.</li>
  569. <li>Edit compare.ini to have the following content:
  570. <pre class="prettyprint linenums">
  571. [Plugin]
  572. version: 1.0
  573. package: myext
  574. module: compare
  575. class: Plugin
  576. depends: None
  577. actions: compare
  578. enabled: True
  579. </pre>
  580. This file is a manifest for Metrix++ plugin loader. Actions field has got new value 'compare'.
  581. Metrix++ engine will automatically pick this action and will add it to the list of available commands.
  582. This plugin will be loaded on 'compare' action.
  583. </li>
  584. <li>Now run Metrix++ to see how this new plugin works:</li>
  585. <pre>&gt; python "/path/to/metrix++.py" compare -- path1 path2 path3</pre>
  586. <pre>["path1", "path2", "path3"]</pre>
  587. </ol>
  588. <h4>Access data file loader and its' interfaces</h4>
  589. <ol>
  590. <li>By default, all post-analysis tools have got --db-file and --db-file-prev options. It is
  591. because 'mpp.dbf' plugin is loaded for any action, including our new one 'compare'. In order to continue
  592. the tutorial, we need to have 2 data files with 'std.code.lines:total' metric collected.
  593. So, write to files by running:</li>
  594. <pre>cd my_project_version_1
  595. &gt; python "/path/to/metrix++.py" collect --std.code.lines.total</pre>
  596. <pre>cd my_project_version_2
  597. &gt; python "/path/to/metrix++.py" collect --std.code.lines.total</pre>
  598. <li>Edit compare.py file to get the loader and iterate collected file paths:</li>
  599. <pre class="prettyprint linenums">
  600. import mpp.api
  601. # load common utils for post processing tools
  602. import mpp.utils
  603. class Plugin(mpp.api.Plugin, mpp.api.IRunable):
  604. def run(self, args):
  605. # get data file reader using standard metrix++ plugin
  606. loader = self.get_plugin('mpp.dbf').get_loader()
  607. # iterate and print file length for every path in args
  608. exit_code = 0
  609. for path in (args if len(args) > 0 else [""]):
  610. file_iterator = loader.iterate_file_data(path)
  611. if file_iterator == None:
  612. mpp.utils.report_bad_path(path)
  613. exit_code += 1
  614. continue
  615. for file_data in file_iterator:
  616. print file_data.get_path()
  617. return exit_code
  618. </pre>
  619. <li>Now run Metrix++ to see how it works:</li>
  620. <pre>&gt; python "/path/to/metrix++.py" compare --db-file=my_project_version_2/metrixpp.db --db-file-prev=my_project_version_1/metrixpp.db</pre>
  621. </ol>
  622. <h4>Identify added, modified files/regions and read metric data</h4>
  623. <ol>
  624. <li>Let's extend the logic of the tool to compare files and regions, read 'std.code.lines:total' metric
  625. and calcuate the summary of number of added lines. mpp.utils.FileRegionsMatcher is helper class
  626. which does matching and comparison of regions for 2 given mpp.api.FileData objects.</li>
  627. <pre class="prettyprint linenums">
  628. import mpp.api
  629. import mpp.utils
  630. import mpp.cout
  631. class Plugin(mpp.api.Plugin, mpp.api.IRunable):
  632. def run(self, args):
  633. loader = self.get_plugin('mpp.dbf').get_loader()
  634. # get previous db file loader
  635. loader_prev = self.get_plugin('mpp.dbf').get_loader_prev()
  636. exit_code = 0
  637. for path in (args if len(args) > 0 else [""]):
  638. added_lines = 0
  639. file_iterator = loader.iterate_file_data(path)
  640. if file_iterator == None:
  641. mpp.utils.report_bad_path(path)
  642. exit_code += 1
  643. continue
  644. for file_data in file_iterator:
  645. added_lines += self._compare_file(file_data, loader, loader_prev)
  646. mpp.cout.notify(path, '', mpp.cout.SEVERITY_INFO,
  647. "Change trend report",
  648. [('Added lines', added_lines)])
  649. return exit_code
  650. def _compare_file(self, file_data, loader, loader_prev):
  651. # compare file with previous and return number of new lines
  652. file_data_prev = loader_prev.load_file_data(file_data.get_path())
  653. if file_data_prev == None:
  654. return self._sum_file_regions_lines(file_data)
  655. elif file_data.get_checksum() != file_data_prev.get_checksum():
  656. return self._compare_file_regions(file_data, file_data_prev)
  657. def _sum_file_regions_lines(self, file_data):
  658. # just sum up the metric for all regions
  659. result = 0
  660. for region in file_data.iterate_regions():
  661. result += region.get_data('std.code.lines', 'total')
  662. def _compare_file_regions(self, file_data, file_data_prev):
  663. # compare every region with previous and return number of new lines
  664. matcher = mpp.utils.FileRegionsMatcher(file_data, file_data_prev)
  665. result = 0
  666. for region in file_data.iterate_regions():
  667. if matcher.is_matched(region.get_id()) == False:
  668. # if added region, just add the lines
  669. result += region.get_data('std.code.lines', 'total')
  670. elif matcher.is_modified(region.get_id()):
  671. # if modified, add the difference in lines
  672. region_prev = file_data_prev.get_region(
  673. matcher.get_prev_id(region.get_id()))
  674. result += (region.get_data('std.code.lines', 'total') -
  675. region_prev.get_data('std.code.lines', 'total'))
  676. return result
  677. </pre>
  678. <li>Now run Metrix++ to see how it works:</li>
  679. <pre>&gt; python "/path/to/metrix++.py" compare --db-file=my_project_version_2/metrixpp.db --db-file-prev=my_project_version_1/metrixpp.db</pre>
  680. <pre>
  681. :: info: Change trend report
  682. Added lines : 7
  683. </pre>
  684. </ol>
  685. <h4>Summary</h4>
  686. <p>We have finished with the tutorial. The tutorial explained how to read Metrix++ data files and
  687. implement custom post-processing tools. Even if some existing Metrix++ code requires clean-up and refactoring,
  688. check code of standard tool plugins to learn more techniques.</p>
  689. </section>
  690. <section id="language_plugin">
  691. <h2>Language parser plugin</h2>
  692. <p>Unfortunately, there is no good documentation at this stage for this part.
  693. Briefly, if metric plugin counts and stores data into FileData object,
  694. tool plugin reads this data, language plugin construct the original structure of
  695. FileData object. The orginal structure includes regions (like functions, classes, etc.)
  696. and markers (like comments, strings, preprocessor, etc.).
  697. Check code of existing parsers.</p>
  698. <ul>
  699. <li>a language parser plugin is registered in the same way as a metric plugin</li>
  700. <li>it registers parser's callback in 'std.tools.collect' plugin</li>
  701. <li>parses a file in a callback, called by 'std.tools.collect'</li>
  702. <li>parser needs to identify markers and regions
  703. and tell about this to file data object passed as an
  704. argument for the callback.</li>
  705. </ul>
  706. <p>There are useful options and tools avaialble for
  707. trobuleshooting purposes during development:</p>
  708. <ul>
  709. <li>metrix++.py debug generates html code showing parsed code structures and their boundaries</li>
  710. <li>--nest-regions for view tool forces the viewer to indent subregions.</li>
  711. <li>--log-level option is available for any command and is helpful to trace execution.</li>
  712. </ul>
  713. <p>Finally, if there are any questions or enquires, please,
  714. feel free to <a href="https://sourceforge.net/p/metrixplusplus/tickets/new/">submit new question</a>.</p>
  715. </section>
  716. </div> <!-- end for sections -->
  717. </div></div> <!-- end for row and container -->
  718. <!-- Footer
  719. ================================================== -->
  720. <footer class="footer">
  721. <div class="container">
  722. <div class="row">
  723. <div class="span3">
  724. <p><a href="http://sourceforge.net/projects/metrixplusplus/"><img src="http://sflogo.sourceforge.net/sflogo.php?group_id=275605&amp;type=3"
  725. alt="Get Metrix++ at SourceForge.net. Fast, secure and Free Open Source software downloads" border="0"></a></p>
  726. <p>&middot;</p>
  727. <p>&middot; &middot;<script type="text/javascript" src="http://www.ohloh.net/p/485947/widgets/project_users_logo.js"></script></p>
  728. <p><a href="http://freecode.com/projects/metrix"><img src="assets/img/fm_logo.png" width="130"></a></p>
  729. <p>&middot;</p>
  730. <p><a href="http://www.softpedia.com/progClean/Metrix-Clean-241097.html"><img src="assets/img/softpedia_free_award_f.gif" width="147" /></a></p>
  731. </div>
  732. <div class="span9">
  733. <p>Copyright <strong>&copy;</strong> 2009 - 2013, <a href="mailto:avkonst@users.sourceforge.net"><span class="normalImportance">Metrix++</span> Project</a></p>
  734. <p>Code licensed under <a href="http://www.gnu.org/licenses/gpl.txt" target="_blank">GPL 3.0</a>, documentation under <a href="http://creativecommons.org/licenses/by/3.0/">CC BY 3.0</a>.</p>
  735. <ul class="footer-links">
  736. <li><a href="https://sourceforge.net/p/metrixplusplus/tickets/new/">Ask question</a></li>
  737. <li class="muted">&middot;</li>
  738. <li><a href="https://sourceforge.net/p/metrixplusplus/tickets/new/">Report defect</a></li>
  739. <li class="muted">&middot;</li>
  740. <li><a href="https://sourceforge.net/p/metrixplusplus/tickets/new/">Feature request</a></li>
  741. <li class="muted">&middot;</li>
  742. <li><a href="https://sourceforge.net/p/metrixplusplus/tickets/search/?q=%21status%3Awont-fix+%26%26+%21status%3Aclosed">Open issues</a></li>
  743. <li class="muted">&middot;</li>
  744. <li><a href="https://sourceforge.net/p/metrixplusplus/wiki/ChangeLog/">Changelog</a></li>
  745. </ul>
  746. </div>
  747. </div>
  748. </div>
  749. </footer>
  750. <!-- Le javascript
  751. ================================================== -->
  752. <!-- Placed at the end of the document so the pages load faster -->
  753. <script type="text/javascript" src="http://platform.twitter.com/widgets.js"></script>
  754. <script src="assets/js/jquery.js"></script>
  755. <script src="assets/js/bootstrap-transition.js"></script>
  756. <script src="assets/js/bootstrap-alert.js"></script>
  757. <script src="assets/js/bootstrap-modal.js"></script>
  758. <script src="assets/js/bootstrap-dropdown.js"></script>
  759. <script src="assets/js/bootstrap-scrollspy.js"></script>
  760. <script src="assets/js/bootstrap-tab.js"></script>
  761. <script src="assets/js/bootstrap-tooltip.js"></script>
  762. <script src="assets/js/bootstrap-popover.js"></script>
  763. <script src="assets/js/bootstrap-button.js"></script>
  764. <script src="assets/js/bootstrap-collapse.js"></script>
  765. <script src="assets/js/bootstrap-carousel.js"></script>
  766. <script src="assets/js/bootstrap-typeahead.js"></script>
  767. <script src="assets/js/bootstrap-affix.js"></script>
  768. <script>
  769. !function ($) {
  770. $(function(){
  771. // carousel demo
  772. $('#myCarousel').carousel()
  773. })
  774. }(window.jQuery)
  775. </script>
  776. <script src="assets/js/holder/holder.js"></script>
  777. <script src="assets/js/google-code-prettify/prettify.js"></script>
  778. <script src="assets/js/application.js"></script>
  779. <script>
  780. </script>
  781. <!-- Analytics
  782. ================================================== -->
  783. <!--
  784. <script>
  785. var _gauges = _gauges || [];
  786. (function() {
  787. var t = document.createElement('script');
  788. t.type = 'text/javascript';
  789. t.async = true;
  790. t.id = 'gauges-tracker';
  791. t.setAttribute('data-site-id', '4f0dc9fef5a1f55508000013');
  792. t.src = '//secure.gaug.es/track.js';
  793. var s = document.getElementsByTagName('script')[0];
  794. s.parentNode.insertBefore(t, s);
  795. })();
  796. </script>
  797. -->
  798. </body>
  799. </html>