Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
2b6a00d
fix: scope codegraph batch complexity targets to file paths
carlos-alm Jul 5, 2026
b28051a
fix: exclude parameters and interface/type members from dead-role cla…
carlos-alm Jul 5, 2026
17fbcba
fix: credit import-type usages as exports consumers
carlos-alm Jul 5, 2026
26918bb
fix: prevent fn-impact/query crash when -f/--file is passed
carlos-alm Jul 5, 2026
be6432c
fix: correct exported-symbol detection for literal and object-literal…
carlos-alm Jul 5, 2026
4ead840
fix: exclude primitive type keywords from ast --kind string matching
carlos-alm Jul 5, 2026
f6326bf
fix: resolve call edges through renamed import specifiers
carlos-alm Jul 5, 2026
590f560
fix: couple file_hashes updates with edge regeneration in incremental…
carlos-alm Jul 5, 2026
d3ea4a4
fix: compare signature-change diffs using new-file line ranges
carlos-alm Jul 5, 2026
c306812
feat: add environment doctor check for stale native binary and missin…
carlos-alm Jul 5, 2026
8bb5893
fix: eliminate non-deterministic ordering in community detection
carlos-alm Jul 6, 2026
46037b1
fix: sync update-graph.sh hook extension allowlist with EXTENSIONS
carlos-alm Jul 6, 2026
6a84cf9
fix: recompute directory structure metrics for affected directories o…
carlos-alm Jul 6, 2026
8d936d1
refactor: register hardcoded execFileSync/execSync maxBuffer values i…
carlos-alm Jul 6, 2026
11c84be
fix: gate blast-radius check on newly introduced risk, not pre-existi…
carlos-alm Jul 6, 2026
963301a
fix: gate identifier-argument dynamic call edges on callback-acceptin…
carlos-alm Jul 6, 2026
3e8035d
fix: gate Array.from's callback arg by position, not just callee name
carlos-alm Jul 6, 2026
879635e
fix: scope reexportedSymbols to actually-named re-export specifiers
carlos-alm Jul 6, 2026
0fe9fc3
fix: stop CFG block/edge count from overriding AST-derived cyclomatic…
carlos-alm Jul 6, 2026
471e856
test: assert cognitive/MI stability against pre-edit baseline too (#1…
carlos-alm Jul 6, 2026
3b3d5f9
fix: resolve merge conflicts with main (docs check acknowledged)
carlos-alm Jul 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 16 additions & 79 deletions src/ast-analysis/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -278,26 +278,19 @@ function storeNativeComplexityResults(
}
}

/** Override a definition's cyclomatic complexity with a CFG-derived value and recompute MI. */
function overrideCyclomaticFromCfg(def: Definition, cfgCyclomatic: number): void {
if (!def.complexity) return;
if (cfgCyclomatic <= 0) {
debug(`overrideCyclomaticFromCfg: skipping ${def.name} — cfgCyclomatic=${cfgCyclomatic}`);
return;
}
def.complexity.cyclomatic = cfgCyclomatic;
const { loc, halstead } = def.complexity;
const volume = halstead ? halstead.volume : 0;
const commentRatio = loc && loc.loc > 0 ? loc.commentLines / loc.loc : 0;
def.complexity.maintainabilityIndex = computeMaintainabilityIndex(
volume,
cfgCyclomatic,
loc?.sloc ?? 0,
commentRatio,
);
}

/** Store native CFG results on definitions, matched by line number. */
/**
* Store native CFG results on definitions, matched by line number.
*
* Intentionally does NOT touch `def.complexity.cyclomatic`: cyclomatic
* complexity is computed once, correctly, from the AST DFS walk (which
* counts short-circuit logical operators, optional chaining, and nested
* function bodies — see computeFunctionComplexity / compute_all_metrics).
* The CFG's block/edge count (E - N + 2) does NOT model any of those, so
* using it to overwrite the AST-derived cyclomatic silently corrupts the
* metric for any function using &&/||/??/?. or containing a closure
* (issue #1743) — CFG blocks/edges are stored here purely for CFG
* queries/visualization (`codegraph cfg`), not as a complexity source.
*/
function storeNativeCfgResults(results: NativeFunctionCfgResult[], defs: Definition[]): void {
const byLine = indexNativeByLine(results);

Expand All @@ -311,48 +304,6 @@ function storeNativeCfgResults(results: NativeFunctionCfgResult[], defs: Definit
const match = matchNativeResult(byLine.get(def.line), def.name);
if (!match) continue;
def.cfg = match.cfg;

// Override complexity cyclomatic with CFG-derived value
const { edges, blocks } = match.cfg;
if (edges && blocks) {
overrideCyclomaticFromCfg(def, edges.length - blocks.length + 2);
}
}
}
}

// ─── CFG cyclomatic reconciliation ──────────────────────────────────────

/**
* Apply CFG-derived cyclomatic override for definitions that already have both
* `complexity` and `cfg` with blocks/edges but whose cyclomatic was never
* overridden (e.g., native extractors provide both fields inline, so the
* normal override path in storeNativeCfgResults / storeCfgResults is skipped).
*/
/** Type guard for cfg objects with blocks and edges arrays. */
function hasCfgBlocksAndEdges(cfg: unknown): cfg is { blocks: unknown[]; edges: unknown[] } {
return (
cfg != null &&
typeof cfg === 'object' &&
Array.isArray((cfg as { blocks?: unknown }).blocks) &&
Array.isArray((cfg as { edges?: unknown }).edges)
);
}

function reconcileCfgCyclomatic(fileSymbols: Map<string, ExtractorOutput>): void {
for (const [, symbols] of fileSymbols) {
const defs = symbols.definitions || [];
for (const def of defs) {
if (
(def.kind === 'function' || def.kind === 'method') &&
def.complexity &&
hasCfgBlocksAndEdges(def.cfg)
) {
const cfgCyclomatic = Math.max(def.cfg.edges.length - def.cfg.blocks.length + 2, 1);
if (cfgCyclomatic !== def.complexity.cyclomatic) {
overrideCyclomaticFromCfg(def, cfgCyclomatic);
}
}
}
}
}
Expand Down Expand Up @@ -610,6 +561,9 @@ function storeComplexityResults(results: WalkResults, defs: Definition[], langId
}
}

// Note: intentionally does not touch def.complexity.cyclomatic — see the
// docblock on storeNativeCfgResults for why the CFG's block/edge count must
// never override the AST-derived cyclomatic complexity (issue #1743).
function storeCfgResults(results: WalkResults, defs: Definition[]): void {
const byLine = indexByLine((results.cfg || []) as CfgFuncResult[]);
for (const def of defs) {
Expand All @@ -621,11 +575,6 @@ function storeCfgResults(results: WalkResults, defs: Definition[]): void {
const cfgResult = matchResultToDef(byLine.get(def.line), def.name);
if (!cfgResult) continue;
def.cfg = { blocks: cfgResult.blocks, edges: cfgResult.edges };

// Override complexity's cyclomatic with CFG-derived value (single source of truth)
if (cfgResult.cyclomatic != null) {
overrideCyclomaticFromCfg(def, cfgResult.cyclomatic);
}
}
}
}
Expand Down Expand Up @@ -868,9 +817,6 @@ async function runFastPathIfApplicable(
if (!allNativeDataComplete(fileSymbols, opts)) return false;

debug('native full-analysis fast path: all data present, skipping WASM/visitor passes');
const doComplexity = opts.complexity !== false;
const doCfg = opts.cfg !== false;
if (doComplexity && doCfg) reconcileCfgCyclomatic(fileSymbols);
await delegateToBuildFunctions(db, fileSymbols, rootDir, opts, engineOpts, timing);
return true;
}
Expand Down Expand Up @@ -912,15 +858,6 @@ export async function runAnalyses(
// per-phase timers above, not additive).
timing._unifiedWalkMs = runUnifiedWalkPass(db, fileSymbols, extToLang, opts, timing);

// Reconcile: apply CFG-derived cyclomatic override for any definitions that have
// both precomputed complexity and CFG data but whose cyclomatic was never overridden.
// This closes a parity gap where native extractors provide both fields inline but
// the override step (storeNativeCfgResults / storeCfgResults) is skipped because
// detectNativeNeeds sees both as already present.
if (doComplexity && doCfg) {
reconcileCfgCyclomatic(fileSymbols);
}

// Delegate to buildXxx functions for DB writes + native fallback
await delegateToBuildFunctions(db, fileSymbols, rootDir, opts, engineOpts, timing);

Expand Down
24 changes: 5 additions & 19 deletions src/domain/wasm-worker-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -516,22 +516,6 @@ function matchResultToDef<T extends { funcNode: TreeSitterNode }>(
);
}

/** Override a definition's cyclomatic complexity with a CFG-derived value and recompute MI. */
function overrideCyclomaticFromCfg(def: Definition, cfgCyclomatic: number): void {
if (!def.complexity) return;
if (cfgCyclomatic <= 0) return;
def.complexity.cyclomatic = cfgCyclomatic;
const { loc, halstead } = def.complexity;
const volume = halstead ? halstead.volume : 0;
const commentRatio = loc && loc.loc > 0 ? loc.commentLines / loc.loc : 0;
def.complexity.maintainabilityIndex = computeMaintainabilityIndex(
volume,
cfgCyclomatic,
loc?.sloc ?? 0,
commentRatio,
);
}

function storeComplexityResults(results: WalkResults, defs: Definition[], langId: string): void {
const byLine = indexByLine((results.complexity || []) as ComplexityFuncResult[]);
for (const def of defs) {
Expand All @@ -555,6 +539,11 @@ function storeComplexityResults(results: WalkResults, defs: Definition[], langId
}
}

// Note: intentionally does not touch def.complexity.cyclomatic with the CFG's
// block/edge count — the CFG doesn't model short-circuit logical operators,
// optional chaining, or nested-function bodies the way the AST-derived
// cyclomatic (storeComplexityResults, above) correctly does, so overriding it
// silently corrupts the metric (issue #1743). Mirrors ast-analysis/engine.ts.
function storeCfgResults(results: WalkResults, defs: Definition[]): void {
const byLine = indexByLine((results.cfg || []) as CfgFuncResult[]);
for (const def of defs) {
Expand All @@ -566,9 +555,6 @@ function storeCfgResults(results: WalkResults, defs: Definition[]): void {
const cfgResult = matchResultToDef(byLine.get(def.line), def.name);
if (!cfgResult) continue;
def.cfg = { blocks: cfgResult.blocks, edges: cfgResult.edges };
if (cfgResult.cyclomatic != null) {
overrideCyclomaticFromCfg(def, cfgResult.cyclomatic);
}
}
}
}
Expand Down
Loading
Loading