One traversal computing cyclomatic + cognitive + loop-nesting + access-depth metrics. Each frame carries its branch-, loop- and access-nesting depth so every metric (cognitive Campbell penalty, loop_depth polynomial-degree proxy, max chained access depth) is produced in a single walk.
| 584 | // every metric (cognitive Campbell penalty, loop_depth polynomial-degree proxy, |
| 585 | // max chained access depth) is produced in a single walk. |
| 586 | void cbm_compute_complexity(TSNode node, const char **branching_types, cbm_complexity_t *out) { |
| 587 | out->cyclomatic = 0; |
| 588 | out->cognitive = 0; |
| 589 | out->loop_count = 0; |
| 590 | out->loop_depth = 0; |
| 591 | out->max_access_depth = 0; |
| 592 | if (!branching_types) { |
| 593 | return; |
| 594 | } |
| 595 | struct cx_frame { |
| 596 | TSNode node; |
| 597 | int bdepth; |
| 598 | int ldepth; |
| 599 | int adepth; |
| 600 | }; |
| 601 | struct cx_frame stack[BRANCHING_STACK_CAP]; |
| 602 | int top = 0; |
| 603 | stack[top].node = node; |
| 604 | stack[top].bdepth = 0; |
| 605 | stack[top].ldepth = 0; |
| 606 | stack[top].adepth = 0; |
| 607 | top++; |
| 608 | while (top > 0) { |
| 609 | struct cx_frame f = stack[--top]; |
| 610 | const char *kind = ts_node_type(f.node); |
| 611 | bool is_branch = false; |
| 612 | for (const char **t = branching_types; *t; t++) { |
| 613 | if (strcmp(kind, *t) == 0) { |
| 614 | is_branch = true; |
| 615 | break; |
| 616 | } |
| 617 | } |
| 618 | int child_b = f.bdepth; |
| 619 | int child_l = f.ldepth; |
| 620 | /* Chained member/subscript access: a.b.c.d nests as access(access(access(a))), |
| 621 | * so each consecutive access node deepens the chain; non-access nodes reset it. */ |
| 622 | int child_a = 0; |
| 623 | if (ts_node_is_named(f.node) && is_member_access_node(kind)) { |
| 624 | child_a = f.adepth + 1; |
| 625 | if (child_a > out->max_access_depth) { |
| 626 | out->max_access_depth = child_a; |
| 627 | } |
| 628 | } |
| 629 | if (is_branch) { |
| 630 | out->cyclomatic++; |
| 631 | out->cognitive += 1 + f.bdepth; /* +1 plus nesting penalty (Campbell) */ |
| 632 | child_b = f.bdepth + 1; |
| 633 | } |
| 634 | /* Only *named* nodes count as loops. In many grammars (Go, C, …) the |
| 635 | * loop's `for`/`while` keyword is an anonymous child token whose node |
| 636 | * type literally equals "for"/"while"; without this guard each loop is |
| 637 | * counted twice and nesting depth is inflated by one. Named loop nodes |
| 638 | * (e.g. Ruby's `while`/`until`/`for`) still match correctly. */ |
| 639 | if (ts_node_is_named(f.node) && cbm_is_loop_node_type(kind)) { |
| 640 | out->loop_count++; |
| 641 | int d = f.ldepth + 1; |
| 642 | if (d > out->loop_depth) { |
| 643 | out->loop_depth = d; |
no test coverage detected