The Understand Python API gives scripts access to everything Understand knows about your code — entities, references, metrics, and dependencies — and powers custom checks, graphs, and reports.

Metrics#

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 Db, an Ent, or an 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 Metric.list to discover available metrics. The returned Metric objects provide metadata such as the id, name, and description.

List catalog metrics that apply to the project (pass a 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 ent.metric, arch.metric, or db.metric for entities, architectures, and databases respectively. Each metric method has the same signature.

The metric is identified by id (str) or by 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 MetricContext object. A metric plugin script that defines multiple metrics can use the id method to find the requested metric id. The db is also available.

The options method can be used to define and retrieve options through an 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 list method. It is not safe to call any other methods that list metrics from inside a metric plugin such as ent.metrics, arch.metrics, db.metrics, or 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 ( ent.metric, arch.metric, or 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 """<p>The number of analysis errors</p>

    <p>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 <a
    href="https://docs.scitools.com/help/accuracy/accurate-c-cpp-projects.html">
    support article  &#8599;</a>.</p>

    <p>See also <a href="und://plugin/metric/CountAnalysisWarning">Analysis
    Warnings</a> for a count of analysis warnings.</p>
    """

  if id == "CountAnalysisWarning":
    return """<p>The number of analysis warnings</p>

    <p>Check out the top level "Project" menu's "Improve Project Accuracy" menu
    for help with analysis accuracy. There's also a <a
    href="https://docs.scitools.com/help/accuracy/accurate-c-cpp-projects.html">
    support article  &#8599;</a>.</p>

    <p>See also <a href="und://plugin/metric/CountAnalysisError">Analysis
    Errors</a> for a count of analysis errors.</p>
    """

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