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, description 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 maps old 6.3 metric ids to the new metric ids.

# A sample metrics plugin.
#
# This plugin maps ids from Understand 6.3 to the new 6.4 ids.

from understand import Arch, Db, Ent, Metric, MetricContext

about_compatibility="""
<p>Some metric ids were changed in Understand 6.4. This plugin supports
the original metric ids by mapping them to the new ones. It's also the sample
plugin shown in Understand's Python API documentation and is useful as a sample
template.</p>
"""


# For this plugin, define the map from old id to new id
metDict = {
  "AltCountLineBlank" :"CountLineBlankWithInactive",
  "AltCountLineCode" :"CountLineCodeWithInactive",
  "AltCountLineComment" :"CountLineCommentWithInactive",
  "CountLineBlank_Html" :"CountLineBlankHtml",
  "CountLineBlank_Javascript" :"CountLineBlankJavascript",
  "CountLineBlank_Php" :"CountLineBlankPhp",
  "CountLineCode_Javascript" :"CountLineCodeJavascript",
  "CountLineCode_Php" :"CountLineCodePhp",
  "CountLineComment_Html" :"CountLineCommentHtml",
  "CountLineComment_Javascript" :"CountLineCommentJavascript",
  "CountLineComment_Php" :"CountLineCommentPhp",
  "CountLine_Html" :"CountLineHtml",
  "CountLine_Javascript" :"CountLineJavascript",
  "CountLine_Php" :"CountLinePhp",
  "AltAvgLineBlank" :"AvgCountLineBlankWithInactive",
  "AltAvgLineCode" :"AvgCountLineCodeWithInactive",
  "AltAvgLineComment" :"AvgCountLineCommentWithInactive",
  "AvgLine" :"AvgCountLine",
  "AvgLineBlank" :"AvgCountLineBlank",
  "AvgLineCode" :"AvgCountLineCode",
  "AvgLineComment" :"AvgCountLineComment",
  "CountDeclProgunit" : "CountDeclProgUnit",
  "CountStmtDecl_Javascript" :"CountStmtDeclJavascript",
  "CountStmtDecl_Php" :"CountStmtDeclPhp",
  "CountStmtExe_Javascript" :"CountStmtExeJavascript",
  "CountStmtExe_Php" :"CountStmtExePhp",
}

def ids() -> str | tuple[str] | list[str]:
  """
  Required, a list of metric ids that this script provides

  For example, CountLineCode is a metric id.
  """
  return list(metDict.keys())

def name(id: str) -> str:
  """
  Required, the name of the metric given by id.

  For example, CountLineCode -> "Source Lines of Code"
  """
  # This sample does not provide new names for these metrics.
  return id

def description(id: str) -> str:
  """
  Required, the description of the metric given by id

  For example, CountLineCode -> "Number of lines containing source code"
  """
  # It is safe to call understand.Metric.lookup().description from a plugin,
  # as long as the mapped id is not one of the ids provided by this plugin.
  mapped_id = metDict.get(id)
  mapped_metric = Metric.lookup(mapped_id)
  return (mapped_metric.description() if mapped_metric else mapped_id) + about_compatibility

def tags(id: str) -> list[str]:
  """
  Optional, tags to display in the plugin manager.
  """
  taglist = [
    'Legacy'
  ]
  if 'Alt' in id:
    taglist.extend([
      'Language: C',
      'Language: C++',
      'Target: Files',
      'Target: Classes',
    ])
    if not 'Avg' in id:
      taglist.extend([
        'Target: Functions',
        'Target: Architectures',
        'Target: Project'
      ])
  elif 'Avg' in id:
    taglist.extend([
      'Target: Files',
      'Target: Classes',
      'Language: Any',
    ])
  elif '_' in id:
    taglist.extend([
      'Language: Web',
      'Target: Files',
      'Target: Classes',
      'Target: Project'
    ])
  else: # CountDeclProgunit
    taglist.extend([
      'Language: Fortran',
      'Target: Files',
      'Target: Project',
      'Sample Template' # Pick a metric arbitrarily as the official sample
    ])
  return taglist

def define_options(metric: MetricContext):
  """
  Optional, define options using the metric.options() object.
  """
  pass

def is_integer(id: str) -> bool:
   """
   Optional, return True if the metric value is an integer.

   If this function is not implemented, it is assumed false, meaning the
   value should be represented as a double/float.
   """
   # All the renamed metrics were integer metrics
   return True

# One of the following three test functions should return True.
def test_entity(metric: MetricContext, ent: Ent) -> bool:
  """
  Optional, return True if metric can be calculated for the given entity.
  """
  # Important: It is NOT safe to call ent.metrics() here to check for the
  # existence of the new metric id. That method will include plugin metrics
  # and lead to infinite recursion. Instead, use the metric object to retrieve
  # only built-in metrics.
  return metDict.get(metric.id()) in metric.list(ent)

def test_architecture(metric: MetricContext, arch: Arch) -> bool:
  """
  Optional, return True if metric can be calculated for the given architecture.
  """
  # Important: It is NOT safe to call arch.metrics() here to check for the
  # existence of the new metric id. That method will include plugin metrics
  # and lead to infinite recursion. Instead, use the metric object to retrieve
  # only built-in metrics.
  return metDict.get(metric.id()) in metric.list(arch)

def test_global(metric: MetricContext, db: Db) -> bool:
  """
  Optional, return True if metric can be calculated for the given database.
  """
  # Important: It is NOT safe to call db.metrics() here to check for the
  # existence of the new metric id. That method will include plugin metrics
  # and lead to infinite recursion. Instead, use the metric object to retrieve
  # only built-in metrics.
  return metDict.get(metric.id()) in metric.list(db)

def test_available(metric: MetricContext, entkindstr: str) -> bool:
  """
  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
  """
  # Important: It is NOT safe to call understand.Metric.list() here to check for
  # the existence of the new metric id. That method will include plugin metrics
  # and lead to infinite recursion. Instead, use the metric object to retrieve
  # only built-in metrics.
  return metDict.get(metric.id()) in metric.list(entkindstr)

def value(metric: MetricContext, target: Arch | Db | Ent) -> float | int:
  """
  Required, return the metric value for the target. The target may be
  an entity, architecture, or database depending on which test functions
  returned True.
  """
  id = metDict.get(metric.id())
  # It is safe to get the value of any metric defined outside this plugin
  # using ent.metric(), db.metric(), and arch.metric(). Because the target
  # will have the metric method no matter what kind it is, this line works:
  return float(target.metric([id])[id])

  # But, if different targets need to be treated differently, use isinstance
  # to determine the target:
  #   if isinstance(target, Db):
  #     pass
  #   elif isinstance(target, Arch):
  #     pass
  #   else: # must be entity
  #     pass