diff --git a/crates/codegraph-core/src/complexity.rs b/crates/codegraph-core/src/complexity.rs index c391f8e0..609047a7 100644 --- a/crates/codegraph-core/src/complexity.rs +++ b/crates/codegraph-core/src/complexity.rs @@ -323,14 +323,7 @@ mod tests { let m = compute_js( "function f(x) { if (x > 0) { return 1; } else if (x < 0) { return -1; } else { return 0; } }", ); - assert_eq!(m.cognitive, 4); // +1 if, +1 else-if, +1 else (from else-if's else_clause), +1 else-if cognitive - // Wait, let me recalculate: - // if: cognitive +1 (nesting 0), cyclomatic +1 - // else-if: cognitive +1, cyclomatic +1 (no nesting) - // else: cognitive +1 - // Total cognitive = 3, cyclomatic = 1 + 1 + 1 = 3 - // Hmm, the else clause wrapping the else-if doesn't add anything (it's detected as else-if wrapper) - // So: if (+1 cog, +1 cyc), else-if (+1 cog, +1 cyc), plain else (+1 cog) + // if (+1 cog, +1 cyc), else-if (+1 cog, +1 cyc), plain else (+1 cog) // cognitive = 3, cyclomatic = 3 assert_eq!(m.cognitive, 3); assert_eq!(m.cyclomatic, 3); diff --git a/crates/codegraph-core/src/extractors/go.rs b/crates/codegraph-core/src/extractors/go.rs index 4f3bd62f..edf18c5b 100644 --- a/crates/codegraph-core/src/extractors/go.rs +++ b/crates/codegraph-core/src/extractors/go.rs @@ -111,7 +111,7 @@ fn walk_node(node: &Node, source: &[u8], symbols: &mut FileSymbols) { line: start_line(&member), end_line: Some(end_line(&member)), decorators: None, - complexity: None, + complexity: None, }); } } diff --git a/crates/codegraph-core/src/types.rs b/crates/codegraph-core/src/types.rs index 9fe5bad7..fc790d5f 100644 --- a/crates/codegraph-core/src/types.rs +++ b/crates/codegraph-core/src/types.rs @@ -6,6 +6,7 @@ use serde::{Deserialize, Serialize}; pub struct ComplexityMetrics { pub cognitive: u32, pub cyclomatic: u32, + #[napi(js_name = "maxNesting")] pub max_nesting: u32, } diff --git a/src/complexity.js b/src/complexity.js index 34614a49..81a0c78f 100644 --- a/src/complexity.js +++ b/src/complexity.js @@ -581,7 +581,21 @@ export async function buildComplexityMetrics(db, fileSymbols, rootDir, _engineOp row.id, def.complexity.cognitive, def.complexity.cyclomatic, - def.complexity.maxNesting ?? def.complexity.max_nesting ?? 0, + def.complexity.maxNesting ?? 0, + 0, + 0, + 0, // loc, sloc, commentLines + 0, + 0, + 0, + 0, // halstead n1, n2, bigN1, bigN2 + 0, + 0, + 0, // vocabulary, length, volume + 0, + 0, + 0, // difficulty, effort, bugs + 0, // maintainabilityIndex ); analyzed++; continue; diff --git a/src/config.js b/src/config.js index b0bceaac..efe38ba9 100644 --- a/src/config.js +++ b/src/config.js @@ -26,9 +26,9 @@ export const DEFAULTS = { ci: { failOnCycles: false, impactThreshold: null }, manifesto: { rules: { - cognitive: { warn: 15, fail: null }, - cyclomatic: { warn: 10, fail: null }, - maxNesting: { warn: 4, fail: null }, + cognitive: { warn: 15 }, + cyclomatic: { warn: 10 }, + maxNesting: { warn: 4 }, maintainabilityIndex: { warn: 20, fail: null }, importCount: { warn: null, fail: null }, exportCount: { warn: null, fail: null }, diff --git a/src/manifesto.js b/src/manifesto.js index 54638a5d..8fc907ff 100644 --- a/src/manifesto.js +++ b/src/manifesto.js @@ -12,18 +12,26 @@ import { debug } from './logger.js'; * defaults: { warn, fail } — null means disabled */ export const RULE_DEFS = [ - { name: 'cognitive', level: 'function', metric: 'cognitive', defaults: { warn: 15, fail: null } }, + { + name: 'cognitive', + level: 'function', + metric: 'cognitive', + defaults: { warn: 15, fail: null }, + reportOnly: true, + }, { name: 'cyclomatic', level: 'function', metric: 'cyclomatic', defaults: { warn: 10, fail: null }, + reportOnly: true, }, { name: 'maxNesting', level: 'function', metric: 'max_nesting', defaults: { warn: 4, fail: null }, + reportOnly: true, }, { name: 'importCount', @@ -67,7 +75,7 @@ function resolveRules(userRules) { const user = userRules?.[def.name]; resolved[def.name] = { warn: user?.warn !== undefined ? user.warn : def.defaults.warn, - fail: user?.fail !== undefined ? user.fail : def.defaults.fail, + fail: def.reportOnly ? null : user?.fail !== undefined ? user.fail : def.defaults.fail, }; } return resolved; diff --git a/src/parser.js b/src/parser.js index 46ad4312..1af5a527 100644 --- a/src/parser.js +++ b/src/parser.js @@ -136,7 +136,7 @@ function normalizeNativeSymbols(result) { ? { cognitive: d.complexity.cognitive, cyclomatic: d.complexity.cyclomatic, - maxNesting: d.complexity.maxNesting ?? d.complexity.max_nesting, + maxNesting: d.complexity.maxNesting, } : null, })), diff --git a/tests/integration/manifesto.test.js b/tests/integration/manifesto.test.js index bc876012..d3b0c0bb 100644 --- a/tests/integration/manifesto.test.js +++ b/tests/integration/manifesto.test.js @@ -291,6 +291,54 @@ describe('manifestoData', () => { } }); + test('reportOnly complexity rules ignore user-configured fail thresholds', () => { + const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-manifesto-reportonly-')); + fs.mkdirSync(path.join(configDir, '.codegraph')); + const roDbPath = path.join(configDir, '.codegraph', 'graph.db'); + + fs.copyFileSync(dbPath, roDbPath); + + // User attempts to set fail thresholds on complexity rules + fs.writeFileSync( + path.join(configDir, '.codegraphrc.json'), + JSON.stringify({ + manifesto: { + rules: { + cognitive: { warn: 15, fail: 5 }, + cyclomatic: { warn: 10, fail: 3 }, + maxNesting: { warn: 4, fail: 2 }, + }, + }, + }), + ); + + const origCwd = process.cwd(); + try { + process.chdir(configDir); + const data = manifestoData(roDbPath); + + // fail thresholds should be forced to null (reportOnly) + for (const name of ['cognitive', 'cyclomatic', 'maxNesting']) { + const rule = data.rules.find((r) => r.name === name); + expect(rule.thresholds.fail).toBeNull(); + } + + // All violations should be warn-only, never fail + const complexityViolations = data.violations.filter( + (v) => v.rule === 'cognitive' || v.rule === 'cyclomatic' || v.rule === 'maxNesting', + ); + for (const v of complexityViolations) { + expect(v.level).toBe('warn'); + } + + // Overall result should pass (no fail-level violations) + expect(data.passed).toBe(true); + } finally { + process.chdir(origCwd); + fs.rmSync(configDir, { recursive: true, force: true }); + } + }); + test('noCycles rule with fail threshold sets passed=false', () => { const configDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codegraph-manifesto-fail-')); fs.mkdirSync(path.join(configDir, '.codegraph'));