libcpg exposes feature-free, read-only metrics for two related questions:
- how a nominal type's methods depend on its fields, inheritance, and resolved calls; and
- how much operator vocabulary and control-flow nesting one function's CPG scope contains.
The first family is the Chidamber–Kemerer (CK) suite plus Lack of Cohesion in
Methods variants LCOM1 through LCOM5. The second family is Halstead volume,
McCabe cyclomatic complexity, Campbell cognitive complexity, and the
Oman–Hagemeister maintainability index (MI). A nominal type is a named
Class, Struct, Enum, or Trait declaration. A method-field graph is a
bipartite graph whose declaration partitions are methods and fields and whose
links retain the concrete access-expression node.
Figure 1 — complete metrics pipeline. Source:
code-metrics-pipeline.puml.
use libcpg::{
ck_metrics, function_metrics, method_field_graph, AnalysisError,
CkOptions, CodePropertyGraph,
};
fn inspect(cpg: &CodePropertyGraph) -> Result<(), AnalysisError> {
for class in ck_metrics(cpg, &CkOptions::default()) {
println!(
"{:?}: WMC={} RFC={} LCOM4={}",
class.class, class.wmc, class.rfc, class.lcom.lcom4,
);
let evidence = method_field_graph(cpg, class.class);
assert_eq!(evidence.lcom(), class.lcom);
for access in evidence.accesses {
println!(
"method {:?} accesses field {:?}: read={} write={}",
access.method, access.field, access.reads, access.writes,
);
}
}
for function in cpg.functions() {
let metrics = function_metrics(cpg, function.id)?;
println!(
"{}: cyclomatic={} cognitive={} volume={:.2} MI={:.2}",
function.name().unwrap_or("<anonymous>"),
metrics.cyclomatic_complexity,
metrics.cognitive_complexity,
metrics.halstead.volume,
metrics.maintainability_index,
);
}
Ok(())
}No Cargo feature is required. Feature serde serializes all public options and
reports. function_metrics returns AnalysisError::UnknownNode or
AnalysisError::NotAFunction for an invalid selector. method_field_graph
instead returns an empty graph carrying the requested ID for a missing or
non-nominal selector; this keeps batch class discovery total.
The analysis does not infer a field from spelling. It admits a method-field link only when the same access node has both kinds of evidence:
access -> FieldasReference; and- a touching
DataFlow(FieldRead)orDataFlow(FieldWrite)edge.
The member-field builder contract
creates these edges for uniquely resolved self.x and this.x. An unresolved
other.x, an ambiguous declaration, or an access without typed DFG evidence
contributes no cohesion link. This creates a false-negative bias instead of
inventing a false relation.
For Rust-style sibling implementation blocks, Impl { for_type: T } contributes
methods only when T identifies exactly one nominal declaration in the same
root/module declaration scope. Fields remain owned by the nominal declaration;
an implementation block cannot introduce a field partition.
MethodFieldGraph exposes four canonical vectors:
| Field | Meaning |
|---|---|
methods |
Direct method IDs, including uniquely associated sibling implementations. |
fields |
Direct field-declaration IDs. |
accesses |
Sorted (method, access, field, reads, writes) evidence. |
method_calls |
Sorted resolved calls whose caller and callee are both methods of the type. |
Every vector is sorted by stable NodeId; repeated CPG storage orders therefore
produce equal reports.
Let
The other fields are:
| Metric | libcpg semantics |
|---|---|
| DIT | Longest parent-component path through selected Inherits edges; a root has depth zero. Inheritance cycles are collapsed before the longest-path pass. |
| NOC | Number of distinct immediate incoming children whose inheritance edge targets the class. |
| CBO | Number of distinct other nominal owners connected by a resolved call in either direction or by a CPG type edge in either direction. |
CkOptions::default() emits Class and Struct rows, excludes Enum rows,
and does not treat Implements as inheritance. include_enums and
include_implements make those choices explicit. Traits/interfaces remain
possible coupling and type-edge endpoints, but do not emit concrete CK rows.
This is a static structural interpretation of the CK suite introduced by
Chidamber and Kemerer in 1991 and refined in 1994. Call-dependent CBO and RFC
consume the same resolved-call semantics as call_graph_projection; unresolved
calls do not invent coupling. See the call-resolution contract.
For each method
The pair-count variants are:
For the graph variants, define
LCOM3 is the Li–Henry connected-set reformulation; LCOM4 is the Hitz–Montazeri extension that admits calls as cohesion evidence. Isolated methods each form a component. A type with no methods has both values zero.
For LCOM5, let
The implementation defines LCOM5 as zero when
function_metrics walks the function's AST scope iteratively and stops before
nested functions. It classifies semantic CpgNodeKind values rather than
lexing source text, so it works when source retention is disabled and across
all frontends that normalize to the common node vocabulary.
McCabe's cyclomatic complexity counts independent control-flow paths. For the
structured CPG vocabulary, libcpg uses the pgmcp-compatible decision form:
Here If, While, For, Loop, and Catch; short-circuit
&&, ||, ??, and, and or; and every direct MatchArm beyond the first
for a Match. The result is at least one and uses saturating u32 arithmetic.
Cognitive complexity measures nesting burden rather than independent paths.
Each If, loop, Match, or Catch contributes Break,
Continue, or resolved self-recursive call contributes one. Nested functions
are independent scopes. These rules follow Campbell's published overview and
the shared pgmcp scoring kernel; they intentionally remain an inspectable
language-neutral subset rather than pretending to model every language's
surface syntax.
Let HalsteadMetrics stores these
four counts plus:
Volume
The semantic classifier treats control constructs, expression operators, calls, member/index access, lambdas, await/yield, and named macros as operators. Parameters, variables, fields, identifiers, member names, literals, generic parameters, and type annotations are operands. Declarations and uses of the same symbol spelling share one operand key. Literal kinds and values remain distinct. Delimiters and discarded grammar tokens are not counted, so the values are comparable between CPGs built under the same normalization contract, not byte-for-byte interchangeable with a token-level Halstead tool.
Let
The result is clamped directly to
build_method_field_graph(C):
collect direct methods and fields below C, stopping at nested declarations
attach sibling Impl(T) methods only when T resolves uniquely beside C
for each method m:
visit m's function scope, stopping before nested functions
for each access a with a --Reference--> owned Field f:
inspect typed DFG edges touching a
if FieldRead or FieldWrite exists: retain (m, a, f, read, write)
retain resolved call pairs whose endpoints are both methods of C
sort every public vector by stable NodeId
compute_ck(C):
WMC = sum function cyclomatic values for C's methods
collapse inheritance cycles and compute longest root distance
count immediate children
aggregate distinct external call/type owners for CBO
union own methods with direct resolved callees for RFC
compute LCOM1-5 from the method-field graph
compute_function_metrics(f):
obtain f's finite AST scope, excluding nested functions
count structured decisions and cognitive increments
classify semantic operator and operand keys
merge clipped comment-line intervals
evaluate Halstead and MI formulae with defined zero cases
All traversals are bounded by the finite input CPG. The analysis has no exponential search, convergence loop, recursion, I/O, or graph mutation. Ordered maps/sets and final stable-ID sorting remove petgraph insertion-order effects. Inheritance cycles are condensed before DIT calculation, preventing a malformed cycle from manufacturing unbounded depth.
For
Metrics summarize represented structure. They do not establish defects, maintainability, comprehension time, or architectural quality by themselves. In particular:
- an unresolved field or call lowers observed coupling/cohesion evidence;
- generated accessors can increase method counts while still connecting LCOM4;
- inherited methods are not copied into a child's method set;
- dynamic dispatch contributes only when the CPG records a resolved target;
- semantic Halstead counts depend on frontend normalization; and
- MI is a comparative index, not a calibrated probability of maintenance cost.
Retain the CPG, builder/resolver configuration, and CkOptions beside any
stored report. Compare values only under the same evidence policy.
See Theory 22, ADR-0029, and Scientific validation 24.
- McCabe, T. J. (1976). A Complexity Measure. DOI: 10.1109/TSE.1976.233837.
- Chidamber, S. R., and Kemerer, C. F. (1991). Towards a Metrics Suite for Object Oriented Design. DOI: 10.1145/117954.117970.
- Chidamber, S. R., and Kemerer, C. F. (1994). A Metrics Suite for Object Oriented Design. DOI: 10.1109/32.295895.
- Li, W., and Henry, S. (1993). Object-Oriented Metrics that Predict Maintainability. DOI: 10.1016/0164-1212(93)90077-B.
- Hitz, M., and Montazeri, B. (1995). Measuring Coupling and Cohesion in Object-Oriented Systems. Proceedings of the International Symposium on Applied Corporate Computing. Author-hosted paper. No DOI was assigned.
- Henderson-Sellers, B. (1996). Object-Oriented Metrics: Measures of Complexity. Prentice Hall PTR. ISBN 0-13-239872-9. No DOI was assigned.
- Halstead, M. H. (1977). Elements of Software Science. Elsevier/North-Holland. ISBN 0-444-00205-7. No DOI was assigned.
- Campbell, G. A. (2018). Cognitive Complexity: An Overview and Evaluation. DOI: 10.1145/3194164.3194186.
- Oman, P., and Hagemeister, J. (1992). Metrics for Assessing a Software System's Maintainability. DOI: 10.1109/ICSM.1992.242525.
- Coleman, D., Ash, D., Lowther, B., and Oman, P. (1994). Using Metrics to Evaluate Software System Maintainability. DOI: 10.1109/2.303623.