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 0Inheritance 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.