Metrics ======= .. toctree:: :hidden: api/understand.Metric api/understand.MetricContext A metric is a numeric value about the code such as the lines of code or cyclomatic complexity. Many metrics are built into Understand and additional metrics can be defined through plugins. Metrics are used for many charts, and to create color scales in graphs. They are also displayed in table views such as the Understand GUI's Entity Locator, allowing for sorting. Discovery --------- Available metrics depend on the target: a :class:`Db `, an :class:`Ent `, or an :class:`Arch `. For example, function entities have values for Cyclomatic but file entities, databases, and architectures only have the aggregate AvgCyclomatic, MaxCyclomatic, and SumCyclomatic metrics. Use :meth:`Metric.list ` to discover available metrics. The returned :class:`Metric ` objects provide metadata such as the :meth:`id `, :meth:`name `, and :meth:`description `. List catalog metrics that apply to the project (pass a :class:`Db ` so availability matches the open database):: import understand db = understand.open("/path/to/myproject.und") for metric in understand.Metric.list(db): print(metric.id(), "-", metric.name()) Generation ---------- To compute values from scripts, call :meth:`ent.metric `, :meth:`arch.metric `, or :meth:`db.metric ` for entities, architectures, and databases respectively. Each metric method has the same signature. The ``metric`` is identified by id (str) or by :class:`Metric ` objects. Metric can be a single metric or a list. For historical reasons, integer valued metrics are returned as Python int objects but real valued metrics are returned as formatted strings. Use the ``format`` argument to control the output format. Compute every **project-level** metric at once:: import understand db = understand.open("/path/to/myproject.und") metrics = understand.Metric.list(db) for metric, value in db.metric(metrics).items(): print(f"{metric.id()} = {value}") Read **Cyclomatic** complexity for each function-like entity:: import understand db = understand.open("/path/to/myproject.und") for func in db.ents("function,method,procedure"): cyclo = func.metric("Cyclomatic") if cyclo is not None: print(f"{func} = {cyclo}") Plugin writing -------------- A metric plugin must have the methods `ids`, `name`, and `value`. A single script may define multiple metrics by returning multiple ids from the `ids` function. At least one `test_` function should be defined to indicate when the metric is available. The parameter `metric` passed to `test_*` and `value` is an :class:`MetricContext ` object. A metric plugin script that defines multiple metrics can use the :meth:`id ` method to find the requested metric id. The :meth:`db ` is also available. The :meth:`options ` method can be used to define and retrieve options through an :class:`options ` object. Metric options are project specific and configured through the project configuration dialog. A metric must be enabled to be visible in project configuration. Important: If the plugin needs to know what other metrics are available for an entity, architecture, or database, it must use the `metric` parameter's :meth:`list ` method. It is not safe to call any other methods that list metrics from inside a metric plugin such as :meth:`ent.metrics `, :meth:`arch.metrics `, :meth:`db.metrics `, or :meth:`Metric.list `. Those metric list functions may call back into the metrics plugin leading to infinite recursion. It is safe to retrieve metric values using the normal metric methods ( :meth:`ent.metric `, :meth:`arch.metric `, or :meth:`db.metric `). The sample below reports the number of analysis errors and warnings for a file, an architecture, or the whole project, and also demonstrates a **per-line** metric with ``test_line``/``lines``. :: # A sample metrics plugin. # # Reports the number of analysis errors and warnings for a file, a function, an # architecture, or the whole project, plus a per-line breakdown. import understand # This import is part of plugins/Shared import und_lib.kind_util as kind_util metDict = { "CountAnalysisError" : ["Analysis Errors", "The number of analysis errors"], "CountAnalysisWarning" : ["Analysis Warnings", "The number of analysis warnings"] } def ids(): """ Required, a list of metric ids that this script provides. """ return [ "CountAnalysisError", "CountAnalysisWarning", ] def name(id): """ Required, the name of the metric given by id. """ return { "CountAnalysisError" : "Analysis Errors", "CountAnalysisWarning": "Analysis Warnings", }.get(id, "") def description(id): """ Optional, the description of the metric given by id. """ if id == "CountAnalysisError": return """

The number of analysis errors

It's important to fix analysis errors to get an accurate project. Check out the top level "Project" menu's "Improve Project Accuracy" menu for help fixing analysis errors. There's also a support article ↗.

See also Analysis Warnings for a count of analysis warnings.

""" if id == "CountAnalysisWarning": return """

The number of analysis warnings

Check out the top level "Project" menu's "Improve Project Accuracy" menu for help with analysis accuracy. There's also a support article ↗.

See also Analysis Errors for a count of analysis errors.

""" def tags(id): """ Optional, tags to display in the plugin manager. """ taglist = [ 'Target: Functions', 'Target: Files', 'Target: Architectures', 'Target: Project', 'Language: Any', 'Line Metric', ] if id == "CountAnalysisError": taglist.append('Sample Template') # Pick a metric arbitrarily as the official sample return taglist def define_options(metric): """ Optional, define options using the metric.options() object. """ pass def is_integer(id): """ Optional, return True if the metric value is an integer. If this function it not implemented, it is assumed false, meaning the value should be represented as a double/float. """ return True # One of the following three test functions should return True. def test_entity(metric, ent): """ Optional, return True if metric can be calculated for the given entity. """ return ent.kind().check(kind_util.LEXER_ENTS_KIND_STR) def test_architecture(metric, arch): """ Optional, return True if metric can be calculated for the given architecture. """ return True def test_global(metric, db): """ Optional, return True if metric can be calculated for the given database. """ return True def test_available(metric,entkindstr): """ Optional, return True if the metric is potentially available. This is used when there isn't a specific target for the metric, like lists of metrics available for export, or for a treemap. Use metric.db() to retrieve the database. If the metric is language specific, the code might look like this: return "Ada" in metric.db().language() entkindstr may be empty. If it is empty, return True as long as the metric is available for an entity, architecture, or the project as a whole. If entkindstr is not empty, return True only if the metric is available for entities matching the provided kind string. Kind checks are performed like this: my_kinds = set(understand.Kind.list_entity(myMetricKindString) test_kinds = set(understand.Kind.list_entity(entkindstr) return len(my_kinds.intersection(test_kinds)) > 0 """ # Kind check if requested if entkindstr: my_kinds = set(kind_util.LEXER_ENTS_KIND_STR) test_kinds = set(understand.Kind.list_entity(entkindstr)) return len(my_kinds.intersection(test_kinds)) > 0 # Violation counts are always available return True def test_line(metric): """ Optional, return True if the metric has values for line. Values per line are returned as a dictionary from line number to line value from the lines function (see below). """ return True def value(metric, target): """ Required, return the metric value for the target. The target may be an entity, architecture, or database depending on which test functions returned True. """ viols = [] if isinstance(target, understand.Arch): for ent in target.ents(True): viols += ent_violations(ent) elif isinstance(target, understand.Db): viols = target.violations() else: viols = ent_violations(target) counts = dict() for v in viols: counts[v.check_id()] = counts.get(v.check_id(),0) + 1 if metric.id() == "CountAnalysisError": return counts.get("UND_ERROR",0) else: return counts.get("UND_WARNING",0) def lines(metric, file): """ Optional, return a dictionary from line number to line value. This method is called if test_line returns True. The dictionary does not have to include values for every line. """ if not file.kind().check("file ~unresolved ~unknown"): return id = "UND_WARNING" if metric.id() == "CountAnalysisError": id = "UND_ERROR" linedict = dict() for v in file.violations(): if v.check_id() == id: linedict[v.line()] = linedict.get(v.line(),0) + 1 return linedict def ent_violations(ent): """ This is a custom helper for this script: return the violations that apply to an entity — a file's own violations, or, for a function, the file's violations restricted to that function's line range. """ if ent.kind().check("file ~unresolved ~unknown"): return ent.violations() if not ent.kind().check(kind_util.FUNCTION_KIND_STR): return [] # Violations not supported defref = ent.ref("begin") if not defref: defref = ent.ref("definein, body declarein") endref = ent.ref("end") if not defref or not endref or defref.file() != endref.file(): return [] viols = [] for v in defref.file().violations(): if v.line() >= defref.line() and v.line() <= endref.line(): viols.append(v) return viols