There are 3 types of plugins considered in this chapter:
Tutorial for metric plugin is generic at the beginning and large portion of this is applied to all other plugins. You need to know python and python regular expressions library to write Metrix++ extensions.
The tutorial will explain how to create a plugin to count magic numbers in source code. It will be relatively simple at first and will be extended with additional configuration options and smarter counting logic.
+ working_directory (set in METRIXPLUSPLUS_PATH variable) \--+ myext \--- __init__.py \--- magic.py \--- magic.ini
import mpp.api
class Plugin(mpp.api.Plugin):
def initialize(self):
print "Hello world"
mpp.api package include Metrix++ API classes. mpp.api.Plugin is the base class, which can be loaded
by Metrix++ engine and does nothing by default. In the code sample above it is extended to print
"Hello world" on initialization.[Plugin] version: 1.0 package: myext module: magic class: Plugin depends: None actions: collect enabled: TrueThis file is a manifest for Metrix++ plugin loader. The fields in Plugin section are:
> python "/path/to/metrix++.py" collect
Hello world
import mpp.api
class Plugin(mpp.api.Plugin,
# make this instance configurable...
mpp.api.IConfigurable):
# ... and implement 2 interfaces
def declare_configuration(self, parser):
parser.add_option("--myext.magic.numbers", "--mmn",
action="store_true", default=False,
help="Enables collection of magic numbers metric [default: %default]")
def configure(self, options):
self.is_active_numbers = options.__dict__['myext.magic.numbers']
def initialize(self):
# use configuration option here
if self.is_active_numbers == True:
print "Hello world"
parser argument is an instance of optparse.OptionParser class. It has got an extension to
accept multiple options of the same argument. Check std.tools.limit to see how to declare multiopt options, if you need.> python "/path/to/metrix++.py" collect --myext.magic.numbers
Hello world
import mpp.api
class Plugin(mpp.api.Plugin,
mpp.api.IConfigurable,
# declare that it can subscribe on notifications
mpp.api.Child):
def declare_configuration(self, parser):
parser.add_option("--myext.magic.numbers", "--mmn",
action="store_true", default=False,
help="Enables collection of magic numbers metric [default: %default]")
def configure(self, options):
self.is_active_numbers = options.__dict__['myext.magic.numbers']
def initialize(self):
if self.is_active_numbers == True:
# subscribe to notifications from all code parsers
self.subscribe_by_parents_interface(mpp.api.ICode, 'callback')
# parents (code parsers) will call the callback declared
def callback(self, parent, data, is_updated):
print parent.get_name(), data.get_path(), is_updated
> python "/path/to/metrix++.py" collect --myext.magic.numbers
std.code.cpp ./test.cpp True
import mpp.api
import re
class Plugin(mpp.api.Plugin,
mpp.api.IConfigurable,
mpp.api.Child,
# reuse by inheriting standard metric facilities
mpp.api.MetricPluginMixin):
def declare_configuration(self, parser):
parser.add_option("--myext.magic.numbers", "--mmn",
action="store_true", default=False,
help="Enables collection of magic numbers metric [default: %default]")
def configure(self, options):
self.is_active_numbers = options.__dict__['myext.magic.numbers']
def initialize(self):
# declare metric rules
self.declare_metric(
self.is_active_numbers, # to count if active in callback
self.Field('numbers', int), # field name and type in the database
re.compile(r'''\b[0-9]+\b'''), # pattern to search
marker_type_mask=mpp.api.Marker.T.CODE, # search in code
region_type_mask=mpp.api.Region.T.ANY) # search in all types of regions
# use superclass facilities to initialize everything from declared fields
super(Plugin, self).initialize(fields=self.get_fields())
# subscribe to all code parsers if at least one metric is active
if self.is_active() == True:
self.subscribe_by_parents_interface(mpp.api.ICode)
> python "/path/to/metrix++.py" collect --myext.magic.numbers
> python "/path/to/metrix++.py" view
:: info: Overall metrics for 'myext.magic:numbers' metric Average : 2.75 Minimum : 0 Maximum : 7 Total : 11.0 Distribution : 4 regions in total (including 0 suppressed) Metric value : Ratio : R-sum : Number of regions 0 : 0.250 : 0.250 : 1 ||||||||||||||||||||||||| 1 : 0.250 : 0.500 : 1 ||||||||||||||||||||||||| 3 : 0.250 : 0.750 : 1 ||||||||||||||||||||||||| 7 : 0.250 : 1.000 : 1 ||||||||||||||||||||||||| :: info: Directory content: Directory : .
import mpp.api
import re
class Plugin(mpp.api.Plugin,
mpp.api.IConfigurable,
mpp.api.Child,
mpp.api.MetricPluginMixin):
def declare_configuration(self, parser):
parser.add_option("--myext.magic.numbers", "--mmn",
action="store_true", default=False,
help="Enables collection of magic numbers metric [default: %default]")
def configure(self, options):
self.is_active_numbers = options.__dict__['myext.magic.numbers']
def initialize(self):
# improve pattern to find declarations of constants
pattern_to_search = re.compile(
r'''((const(\s+[_a-zA-Z][_a-zA-Z0-9]*)+\s*[=]\s*)[-+]?[0-9]+\b)|(\b[0-9]+\b)''')
self.declare_metric(self.is_active_numbers,
self.Field('numbers', int),
# give a pair of pattern + custom counter logic class
(pattern_to_search, self.NumbersCounter),
marker_type_mask=mpp.api.Marker.T.CODE,
region_type_mask=mpp.api.Region.T.ANY)
super(Plugin, self).initialize(fields=self.get_fields())
if self.is_active() == True:
self.subscribe_by_parents_interface(mpp.api.ICode)
# implement custom counter behavior:
# increments counter by 1 only if single number spotted,
# but not declaration of a constant
class NumbersCounter(mpp.api.MetricPluginMixin.IterIncrementCounter):
def increment(self, match):
if match.group(0).startswith('const'):
return 0
return 1
> python "/path/to/metrix++.py" collect --myext.magic.numbers
> python "/path/to/metrix++.py" view
import mpp.api
import re
class Plugin(mpp.api.Plugin,
mpp.api.IConfigurable,
mpp.api.Child,
mpp.api.MetricPluginMixin):
def declare_configuration(self, parser):
parser.add_option("--myext.magic.numbers", "--mmn",
action="store_true", default=False,
help="Enables collection of magic numbers metric [default: %default]")
def configure(self, options):
self.is_active_numbers = options.__dict__['myext.magic.numbers']
def initialize(self):
# specialized pattern for java
pattern_to_search_java = re.compile(
r'''((const(\s+[_$a-zA-Z][_$a-zA-Z0-9]*)+\s*[=]\s*)[-+]?[0-9]+\b)|(\b[0-9]+\b)''')
# pattern for C++ and C# languages
pattern_to_search_cpp_cs = re.compile(
r'''((const(\s+[_a-zA-Z][_a-zA-Z0-9]*)+\s*[=]\s*)[-+]?[0-9]+\b)|(\b[0-9]+\b)''')
# pattern for all other languages
pattern_to_search = re.compile(
r'''\b[0-9]+\b''')
self.declare_metric(self.is_active_numbers,
self.Field('numbers', int),
# dictionary of pairs instead of a single pair
{
'std.code.java': (pattern_to_search_java, self.NumbersCounter),
'std.code.cpp': (pattern_to_search_cpp_cs, self.NumbersCounter),
'std.code.cs': (pattern_to_search_cpp_cs, self.NumbersCounter),
'*': pattern_to_search
},
marker_type_mask=mpp.api.Marker.T.CODE,
region_type_mask=mpp.api.Region.T.ANY)
super(Plugin, self).initialize(fields=self.get_fields())
if self.is_active() == True:
self.subscribe_by_parents_interface(mpp.api.ICode)
class NumbersCounter(mpp.api.MetricPluginMixin.IterIncrementCounter):
def increment(self, match):
if match.group(0).startswith('const'):
return 0
return 1
> python "/path/to/metrix++.py" collect --myext.magic.numbers
> python "/path/to/metrix++.py" view
import mpp.api
import re
class Plugin(mpp.api.Plugin,
mpp.api.IConfigurable,
mpp.api.Child,
mpp.api.MetricPluginMixin):
def declare_configuration(self, parser):
parser.add_option("--myext.magic.numbers", "--mmn",
action="store_true", default=False,
help="Enables collection of magic numbers metric [default: %default]")
def configure(self, options):
self.is_active_numbers = options.__dict__['myext.magic.numbers']
def initialize(self):
pattern_to_search_java = re.compile(
r'''((const(\s+[_$a-zA-Z][_$a-zA-Z0-9]*)+\s*[=]\s*)[-+]?[0-9]+\b)|(\b[0-9]+\b)''')
pattern_to_search_cpp_cs = re.compile(
r'''((const(\s+[_a-zA-Z][_a-zA-Z0-9]*)+\s*[=]\s*)[-+]?[0-9]+\b)|(\b[0-9]+\b)''')
pattern_to_search = re.compile(
r'''\b[0-9]+\b''')
self.declare_metric(self.is_active_numbers,
self.Field('numbers', int,
# optimize the size of datafile:
# store only non-zero results
non_zero=True),
{
'std.code.java': (pattern_to_search_java, self.NumbersCounter),
'std.code.cpp': (pattern_to_search_cpp_cs, self.NumbersCounter),
'std.code.cs': (pattern_to_search_cpp_cs, self.NumbersCounter),
'*': pattern_to_search
},
marker_type_mask=mpp.api.Marker.T.CODE,
region_type_mask=mpp.api.Region.T.ANY)
super(Plugin, self).initialize(fields=self.get_fields())
if self.is_active() == True:
self.subscribe_by_parents_interface(mpp.api.ICode)
class NumbersCounter(mpp.api.MetricPluginMixin.IterIncrementCounter):
def increment(self, match):
if match.group(0).startswith('const'):
return 0
return 1
> python "/path/to/metrix++.py" collect --myext.magic.numbers
> python "/path/to/metrix++.py" view
import mpp.api
import re
class Plugin(mpp.api.Plugin,
mpp.api.IConfigurable,
mpp.api.Child,
mpp.api.MetricPluginMixin):
def declare_configuration(self, parser):
parser.add_option("--myext.magic.numbers", "--mmn",
action="store_true", default=False,
help="Enables collection of magic numbers metric [default: %default]")
# Add new option
parser.add_option("--myext.magic.numbers.simplier", "--mmns",
action="store_true", default=False,
help="Is set, 0, -1 and 1 numbers are not counted [default: %default]")
def configure(self, options):
self.is_active_numbers = options.__dict__['myext.magic.numbers']
# remember the option here
self.is_active_numbers_simplier = options.__dict__['myext.magic.numbers.simplier']
def initialize(self):
pattern_to_search_java = re.compile(
r'''((const(\s+[_$a-zA-Z][_$a-zA-Z0-9]*)+\s*[=]\s*)[-+]?[0-9]+\b)|(\b[0-9]+\b)''')
pattern_to_search_cpp_cs = re.compile(
r'''((const(\s+[_a-zA-Z][_a-zA-Z0-9]*)+\s*[=]\s*)[-+]?[0-9]+\b)|(\b[0-9]+\b)''')
pattern_to_search = re.compile(
r'''\b[0-9]+\b''')
self.declare_metric(self.is_active_numbers,
self.Field('numbers', int,
non_zero=True),
{
'std.code.java': (pattern_to_search_java, self.NumbersCounter),
'std.code.cpp': (pattern_to_search_cpp_cs, self.NumbersCounter),
'std.code.cs': (pattern_to_search_cpp_cs, self.NumbersCounter),
'*': pattern_to_search
},
marker_type_mask=mpp.api.Marker.T.CODE,
region_type_mask=mpp.api.Region.T.ANY)
super(Plugin, self).initialize(fields=self.get_fields(),
# remember option settings in data file properties
# in order to detect changes in settings on iterative re-run
properties=[self.Property('number.simplier', self.is_active_numbers_simplier)])
if self.is_active() == True:
self.subscribe_by_parents_interface(mpp.api.ICode)
class NumbersCounter(mpp.api.MetricPluginMixin.IterIncrementCounter):
def increment(self, match):
if (match.group(0).startswith('const') or
(self.plugin.is_active_numbers_simplier == True and
match.group(0) in ['0', '1', '-1', '+1'])):
return 0
return 1
> python "/path/to/metrix++.py" collect --myext.magic.numbers
> python "/path/to/metrix++.py" view
:: info: Overall metrics for 'myext.magic:numbers' metric Average : 2.5 (excluding zero metric values) Minimum : 2 Maximum : 3 Total : 5.0 Distribution : 2 regions in total (including 0 suppressed) Metric value : Ratio : R-sum : Number of regions 2 : 0.500 : 0.500 : 1 |||||||||||||||||||||||||||||||||||||||||||||||||| 3 : 0.500 : 1.000 : 1 |||||||||||||||||||||||||||||||||||||||||||||||||| :: info: Directory content: Directory : .
We have finished with the tutorial. The tutorial explained how to implement simple and advanced metric plugins. We used built-in Metrix++ base classes. If you need to more advanced plugin capabilities, override in your plugin class functions inherited from mpp.api base classes. Check code of standard plugins to learn more techniques.
This tutorial will explain how to build custom Metrix++ command, which is bound to custom post-analysis tool. We will implement the tool, which identifies all new and changed regions and counts number of added lines. We skip calculating number of deleted lines, but it is easy to extend from what we get finally in the tutorial.
+ working_directory (set in METRIXPLUSPLUS_PATH variable) \--+ myext \--- __init__.py \--- compare.py \--- compare.ini
import mpp.api
class Plugin(mpp.api.Plugin, mpp.api.IRunable):
def run(self, args):
print args
return 0
Inheritance from mpp.api.IRunable declares that the plugin is runable and requires implementation of 'run' interface.[Plugin] version: 1.0 package: myext module: compare class: Plugin depends: None actions: compare enabled: TrueThis file is a manifest for Metrix++ plugin loader. Actions field has got new value 'compare'. Metrix++ engine will automatically pick this action and will add it to the list of available commands. This plugin will be loaded on 'compare' action.
> python "/path/to/metrix++.py" compare -- path1 path2 path3
["path1", "path2", "path3"]
cd my_project_version_1 > python "/path/to/metrix++.py" collect --std.code.lines.total
cd my_project_version_2 > python "/path/to/metrix++.py" collect --std.code.lines.total
import mpp.api
# load common utils for post processing tools
import mpp.utils
class Plugin(mpp.api.Plugin, mpp.api.IRunable):
def run(self, args):
# get data file reader using standard metrix++ plugin
loader = self.get_plugin('mpp.dbf').get_loader()
# iterate and print file length for every path in args
exit_code = 0
for path in (args if len(args) > 0 else [""]):
file_iterator = loader.iterate_file_data(path)
if file_iterator == None:
mpp.utils.report_bad_path(path)
exit_code += 1
continue
for file_data in file_iterator:
print file_data.get_path()
return exit_code
> python "/path/to/metrix++.py" compare --db-file=my_project_version_2/metrixpp.db --db-file-prev=my_project_version_1/metrixpp.db
import mpp.api
import mpp.utils
import mpp.cout
class Plugin(mpp.api.Plugin, mpp.api.IRunable):
def run(self, args):
loader = self.get_plugin('mpp.dbf').get_loader()
# get previous db file loader
loader_prev = self.get_plugin('mpp.dbf').get_loader_prev()
exit_code = 0
for path in (args if len(args) > 0 else [""]):
added_lines = 0
file_iterator = loader.iterate_file_data(path)
if file_iterator == None:
mpp.utils.report_bad_path(path)
exit_code += 1
continue
for file_data in file_iterator:
added_lines += self._compare_file(file_data, loader, loader_prev)
mpp.cout.notify(path, '', mpp.cout.SEVERITY_INFO,
"Change trend report",
[('Added lines', added_lines)])
return exit_code
def _compare_file(self, file_data, loader, loader_prev):
# compare file with previous and return number of new lines
file_data_prev = loader_prev.load_file_data(file_data.get_path())
if file_data_prev == None:
return self._sum_file_regions_lines(file_data)
elif file_data.get_checksum() != file_data_prev.get_checksum():
return self._compare_file_regions(file_data, file_data_prev)
def _sum_file_regions_lines(self, file_data):
# just sum up the metric for all regions
result = 0
for region in file_data.iterate_regions():
result += region.get_data('std.code.lines', 'total')
def _compare_file_regions(self, file_data, file_data_prev):
# compare every region with previous and return number of new lines
matcher = mpp.utils.FileRegionsMatcher(file_data, file_data_prev)
result = 0
for region in file_data.iterate_regions():
if matcher.is_matched(region.get_id()) == False:
# if added region, just add the lines
result += region.get_data('std.code.lines', 'total')
elif matcher.is_modified(region.get_id()):
# if modified, add the difference in lines
region_prev = file_data_prev.get_region(
matcher.get_prev_id(region.get_id()))
result += (region.get_data('std.code.lines', 'total') -
region_prev.get_data('std.code.lines', 'total'))
return result
> python "/path/to/metrix++.py" compare --db-file=my_project_version_2/metrixpp.db --db-file-prev=my_project_version_1/metrixpp.db
:: info: Change trend report Added lines : 7
We have finished with the tutorial. The tutorial explained how to read Metrix++ data files and implement custom post-processing tools. Even if some existing Metrix++ code requires clean-up and refactoring, check code of standard tool plugins to learn more techniques.
Unfortunately, there is no good documentation at this stage for this part. Briefly, if metric plugin counts and stores data into FileData object, tool plugin reads this data, language plugin construct the original structure of FileData object. The orginal structure includes regions (like functions, classes, etc.) and markers (like comments, strings, preprocessor, etc.). Check code of existing parsers.
There are useful options and tools avaialble for trobuleshooting purposes during development:
Finally, if there are any questions or enquires, please, feel free to submit new question.