From 4582c165349fbe7d40fa2831687a4bb465787488 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 30 Mar 2026 16:41:30 +0000 Subject: [PATCH 1/4] refactor: extract generic createAnalyzer helper and implement showFunctionDetails MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace four near-identical create*Analyzer functions in metricsAnalyzerFactory.ts with a single generic createAnalyzer(modulePath, className) helper. All four functions shared the same shape: lazy require(), analyzeFile() call, and 0→1 line/column normalization on detail positions. The helper removes ~140 lines of duplication while preserving identical runtime behaviour (Node.js module cache ensures the require() overhead is paid only once per language). - Implement cognitiveComplexity.showFunctionDetails command in extension.ts. Previously a no-op, the command now writes a formatted complexity breakdown to a dedicated 'Code Metrics Details' output channel and reveals it with preserveFocus so the editor cursor stays in place. The channel is created lazily and disposed on deactivation. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/extension.ts | 53 ++++- src/metricsAnalyzer/metricsAnalyzerFactory.ts | 219 +++--------------- 2 files changed, 78 insertions(+), 194 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index a72f4e2..2cdcb3d 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -4,19 +4,58 @@ import * as vscode from "vscode"; import { registerCodeLensProvider } from "./providers/codeLensProvider"; import { UnifiedFunctionMetrics } from "./metricsAnalyzer/metricsAnalyzerFactory"; +/** Shared output channel for function complexity details (created once, reused). */ +let detailsChannel: vscode.OutputChannel | undefined; + +/** + * Formats a cognitive complexity breakdown for a function and writes it to the + * shared output channel, then reveals the channel to the user. + */ +function showFunctionDetails( + func: UnifiedFunctionMetrics, + _uri?: vscode.Uri +): void { + if (!detailsChannel) { + detailsChannel = vscode.window.createOutputChannel("Code Metrics Details"); + } + + detailsChannel.clear(); + detailsChannel.appendLine(`Function: ${func.name}`); + detailsChannel.appendLine(`Cognitive Complexity: ${func.complexity}`); + detailsChannel.appendLine( + `Location: lines ${func.startLine + 1}–${func.endLine + 1}` + ); + + if (func.details.length === 0) { + detailsChannel.appendLine("\nNo complexity contributors — complexity is 0."); + } else { + detailsChannel.appendLine("\nComplexity contributors:"); + detailsChannel.appendLine( + " Line │ +Score │ Nesting │ Reason" + ); + detailsChannel.appendLine( + "────────┼────────┼─────────┼────────────────────────────────────" + ); + for (const d of func.details) { + const line = String(d.line).padStart(6); + const inc = `+${d.increment}`.padStart(6); + const nesting = String(d.nesting).padStart(7); + detailsChannel.appendLine(` ${line} │ ${inc} │ ${nesting} │ ${d.reason}`); + } + } + + detailsChannel.show(true /* preserveFocus */); +} + // This method is called when your extension is activated // Your extension is activated the very first time the command is executed export function activate(context: vscode.ExtensionContext) { console.log("Code Metrics extension is now active!"); - // Register command for CodeLens clicks (no-op to suppress errors) + // Register command for CodeLens clicks — shows a formatted breakdown in the output channel const showFunctionDetailsCommand = vscode.commands.registerCommand( "cognitiveComplexity.showFunctionDetails", - (func: UnifiedFunctionMetrics, uri: vscode.Uri) => { - // No-op implementation: Command registered to prevent 'not found' error when CodeLens is clicked - // Future enhancement: Could show function details in a webview or output channel - // Parameters received: func (function metrics data), uri (document URI) - } + showFunctionDetails ); // Register providers @@ -28,4 +67,6 @@ export function activate(context: vscode.ExtensionContext) { // This method is called when your extension is deactivated export function deactivate() { console.log("Code Metrics extension is now deactivated"); + detailsChannel?.dispose(); + detailsChannel = undefined; } diff --git a/src/metricsAnalyzer/metricsAnalyzerFactory.ts b/src/metricsAnalyzer/metricsAnalyzerFactory.ts index 83558a0..5a90650 100644 --- a/src/metricsAnalyzer/metricsAnalyzerFactory.ts +++ b/src/metricsAnalyzer/metricsAnalyzerFactory.ts @@ -146,62 +146,31 @@ function hashString(str: string): number { } /** - * A record of language-specific analyzers that compute cognitive complexity metrics for source code. - * Each analyzer is a function that takes source text and returns an array of complexity data for all functions found. + * Creates a language-specific cognitive complexity analyzer function. * - * @remarks - * The analyzers normalize line and column numbers to be 1-based across all languages for consistency. - * Each language analyzer is lazily loaded using require() to optimize initial load time. + * All language analyzers share the same output shape and the same normalization + * step (0-based → 1-based line/column in detail positions). This helper + * centralises that logic so individual language registrations stay concise. * - * @example - * ```typescript - * const analyzer = languageAnalyzers['csharp']; - * const complexityData = analyzer(sourceCode); - * ``` + * @param modulePath - require()-style path to the language analyzer module (relative to this file) + * @param className - Name of the exported analyzer class that exposes a static `analyzeFile` method + * @returns A function that takes source text and returns an array of UnifiedFunctionMetrics */ -const languageAnalyzers: Record< - string, - (sourceText: string) => UnifiedFunctionMetrics[] -> = { - csharp: createCSharpAnalyzer(), - go: createGoAnalyzer(), - javascript: createJavaScriptAnalyzer(), - javascriptreact: createJavaScriptAnalyzer(), - typescript: createTypeScriptAnalyzer(), - typescriptreact: createTypeScriptAnalyzer(), -}; - -/** - * Creates a C# cognitive complexity analyzer function. - * - * @returns A function that analyzes C# source code and returns an array of function complexity metrics. - * The returned analyzer function: - * - Takes C# source code as a string parameter - * - Analyzes cognitive complexity of all functions in the code - * - Returns an array of UnifiedFunctionMetrics objects containing: - * - Function name - * - Complexity score - * - Detailed breakdown of complexity increments with line/column positions (1-based indexing) - * - Function boundaries (start/end line and column) - * - * @remarks - * The analyzer dynamically requires the C# analyzer module and normalizes its output - * from 0-based to 1-based line and column indexing for consistency. - */ -function createCSharpAnalyzer(): ( - sourceText: string -) => UnifiedFunctionMetrics[] { - return function (sourceText: string) { - const { CSharpMetricsAnalyzer } = require("./languages/csharpAnalyzer"); - const functions = CSharpMetricsAnalyzer.analyzeFile(sourceText); +function createAnalyzer( + modulePath: string, + className: string +): (sourceText: string) => UnifiedFunctionMetrics[] { + return function (sourceText: string): UnifiedFunctionMetrics[] { + const mod = require(modulePath); + const functions: any[] = mod[className].analyzeFile(sourceText); return functions.map((func: any) => ({ name: func.name, complexity: func.complexity, details: func.details.map((detail: any) => ({ increment: detail.increment, reason: detail.reason, - line: detail.line + 1, // C# analyzer uses 0-based, normalize to 1-based - column: detail.column + 1, // C# analyzer uses 0-based, normalize to 1-based + line: detail.line + 1, // analyzers use 0-based; normalize to 1-based + column: detail.column + 1, // analyzers use 0-based; normalize to 1-based nesting: detail.nesting, })), startLine: func.startLine, @@ -213,147 +182,21 @@ function createCSharpAnalyzer(): ( } /** - * Creates a Go cognitive complexity analyzer function. - * - * @returns A function that analyzes Go source code and returns an array of function complexity metrics. - * The returned analyzer function: - * - Takes Go source code as a string parameter - * - Analyzes cognitive complexity of all functions in the code - * - Returns an array of UnifiedFunctionMetrics objects containing: - * - Function name - * - Complexity score - * - Detailed breakdown of complexity increments with line/column positions (1-based indexing) - * - Function boundaries (start/end line and column) + * A record of language-specific analyzers that compute cognitive complexity metrics for source code. * * @remarks - * The analyzer dynamically requires the Go analyzer module and normalizes the detail positions - * (line and column in the details array) from 0-based to 1-based indexing for consistency - * with the C# analyzer. Function boundary positions remain as returned by the analyzer. - */ -function createGoAnalyzer(): (sourceText: string) => UnifiedFunctionMetrics[] { - return function (sourceText: string) { - const { GoMetricsAnalyzer } = require("./languages/goAnalyzer"); - interface GoMetricsDetail { - increment: number; - reason: string; - line: number; - column: number; - nesting: number; - } - interface GoFunctionMetrics { - name: string; - complexity: number; - details: GoMetricsDetail[]; - startLine: number; - endLine: number; - startColumn: number; - endColumn: number; - } - const functions = GoMetricsAnalyzer.analyzeFile(sourceText) as GoFunctionMetrics[]; - return functions.map((func: GoFunctionMetrics) => ({ - name: func.name, - complexity: func.complexity, - details: func.details.map((detail: GoMetricsDetail) => ({ - increment: detail.increment, - reason: detail.reason, - line: detail.line + 1, // Go analyzer uses 0-based, normalize to 1-based - column: detail.column + 1, // Go analyzer uses 0-based, normalize to 1-based - nesting: detail.nesting, - })), - startLine: func.startLine, - endLine: func.endLine, - startColumn: func.startColumn, - endColumn: func.endColumn, - })); - }; -} - -/** - * Creates a JavaScript cognitive complexity analyzer function. - * - * @returns A function that analyzes JavaScript source code and returns an array of function complexity metrics. - */ -function createJavaScriptAnalyzer(): ( - sourceText: string -) => UnifiedFunctionMetrics[] { - return function (sourceText: string) { - const { JavaScriptMetricsAnalyzer } = require("./languages/javascriptAnalyzer"); - interface JSDetail { - increment: number; - reason: string; - line: number; - column: number; - nesting: number; - } - interface JSFunctionMetrics { - name: string; - complexity: number; - details: JSDetail[]; - startLine: number; - endLine: number; - startColumn: number; - endColumn: number; - } - const functions = JavaScriptMetricsAnalyzer.analyzeFile(sourceText) as JSFunctionMetrics[]; - return functions.map((func: JSFunctionMetrics) => ({ - name: func.name, - complexity: func.complexity, - details: func.details.map((detail: JSDetail) => ({ - increment: detail.increment, - reason: detail.reason, - line: detail.line + 1, // JS analyzer uses 0-based, normalize to 1-based - column: detail.column + 1, // JS analyzer uses 0-based, normalize to 1-based - nesting: detail.nesting, - })), - startLine: func.startLine, - endLine: func.endLine, - startColumn: func.startColumn, - endColumn: func.endColumn, - })); - }; -} - -/** - * Creates a TypeScript cognitive complexity analyzer function. - * - * @returns A function that analyzes TypeScript source code and returns an array of function complexity metrics. + * Line and column numbers in detail positions are normalized to 1-based across all languages. + * Each analyzer module is lazily loaded via require() on first invocation (Node.js caches the module + * afterwards), so startup time is not affected by the number of supported languages. */ -function createTypeScriptAnalyzer(): ( - sourceText: string -) => UnifiedFunctionMetrics[] { - return function (sourceText: string) { - const { TypeScriptMetricsAnalyzer } = require("./languages/typescriptAnalyzer"); - interface TSDetail { - increment: number; - reason: string; - line: number; - column: number; - nesting: number; - } - interface TSFunctionMetrics { - name: string; - complexity: number; - details: TSDetail[]; - startLine: number; - endLine: number; - startColumn: number; - endColumn: number; - } - const functions = TypeScriptMetricsAnalyzer.analyzeFile(sourceText) as TSFunctionMetrics[]; - return functions.map((func: TSFunctionMetrics) => ({ - name: func.name, - complexity: func.complexity, - details: func.details.map((detail: TSDetail) => ({ - increment: detail.increment, - reason: detail.reason, - line: detail.line + 1, // TS analyzer uses 0-based, normalize to 1-based - column: detail.column + 1, // TS analyzer uses 0-based, normalize to 1-based - nesting: detail.nesting, - })), - startLine: func.startLine, - endLine: func.endLine, - startColumn: func.startColumn, - endColumn: func.endColumn, - })); - }; -} +const languageAnalyzers: Record< + string, + (sourceText: string) => UnifiedFunctionMetrics[] +> = { + csharp: createAnalyzer("./languages/csharpAnalyzer", "CSharpMetricsAnalyzer"), + go: createAnalyzer("./languages/goAnalyzer", "GoMetricsAnalyzer"), + javascript: createAnalyzer("./languages/javascriptAnalyzer", "JavaScriptMetricsAnalyzer"), + javascriptreact: createAnalyzer("./languages/javascriptAnalyzer", "JavaScriptMetricsAnalyzer"), + typescript: createAnalyzer("./languages/typescriptAnalyzer", "TypeScriptMetricsAnalyzer"), + typescriptreact: createAnalyzer("./languages/typescriptAnalyzer", "TypeScriptMetricsAnalyzer"), +}; From 98f4df137f27407294a0db300c26ccc7b82d3aba Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:25:29 +0000 Subject: [PATCH 2/4] refactor: add typed interfaces and runtime guard in createAnalyzer; fix empty-details message Agent-Logs-Url: https://github.com/askpt/code-metrics/sessions/3faebe2a-5bfd-4cf2-b9b2-93fdaa3e8e58 Co-authored-by: askpt <2493377+askpt@users.noreply.github.com> --- src/extension.ts | 2 +- src/metricsAnalyzer/metricsAnalyzerFactory.ts | 48 +++++++++++++++++-- 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 2cdcb3d..aca70bb 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -27,7 +27,7 @@ function showFunctionDetails( ); if (func.details.length === 0) { - detailsChannel.appendLine("\nNo complexity contributors — complexity is 0."); + detailsChannel.appendLine("\nNo complexity contributors were reported."); } else { detailsChannel.appendLine("\nComplexity contributors:"); detailsChannel.appendLine( diff --git a/src/metricsAnalyzer/metricsAnalyzerFactory.ts b/src/metricsAnalyzer/metricsAnalyzerFactory.ts index 5a90650..d7c2d98 100644 --- a/src/metricsAnalyzer/metricsAnalyzerFactory.ts +++ b/src/metricsAnalyzer/metricsAnalyzerFactory.ts @@ -145,6 +145,38 @@ function hashString(str: string): number { return hash >>> 0; // Convert to unsigned 32-bit integer } +/** + * Raw detail shape returned by all language-specific analyzers. + * `line` and `column` are 0-based and are normalized to 1-based by createAnalyzer. + */ +interface RawMetricsDetail { + increment: number; + reason: string; + line: number; + column: number; + nesting: number; +} + +/** + * Raw function-level shape returned by all language-specific analyzers. + * All position fields (`startLine`, `endLine`, `startColumn`, `endColumn`) are 0-based + * and are passed through as-is to `UnifiedFunctionMetrics` without normalization. + */ +interface RawFunctionMetrics { + name: string; + complexity: number; + details: RawMetricsDetail[]; + startLine: number; + endLine: number; + startColumn: number; + endColumn: number; +} + +/** Shape of a language analyzer class that must expose a static `analyzeFile` method. */ +interface AnalyzerClass { + analyzeFile(sourceText: string): RawFunctionMetrics[]; +} + /** * Creates a language-specific cognitive complexity analyzer function. * @@ -155,18 +187,26 @@ function hashString(str: string): number { * @param modulePath - require()-style path to the language analyzer module (relative to this file) * @param className - Name of the exported analyzer class that exposes a static `analyzeFile` method * @returns A function that takes source text and returns an array of UnifiedFunctionMetrics + * @throws {Error} If the module does not export the expected class with an `analyzeFile` method */ function createAnalyzer( modulePath: string, className: string ): (sourceText: string) => UnifiedFunctionMetrics[] { return function (sourceText: string): UnifiedFunctionMetrics[] { - const mod = require(modulePath); - const functions: any[] = mod[className].analyzeFile(sourceText); - return functions.map((func: any) => ({ + const mod = require(modulePath) as Record; + const analyzerClass = mod[className]; + if (!analyzerClass || typeof analyzerClass.analyzeFile !== "function") { + throw new Error( + `Analyzer module "${modulePath}" does not export a class named "${className}" ` + + `with a static analyzeFile method.` + ); + } + const functions: RawFunctionMetrics[] = analyzerClass.analyzeFile(sourceText); + return functions.map((func: RawFunctionMetrics) => ({ name: func.name, complexity: func.complexity, - details: func.details.map((detail: any) => ({ + details: func.details.map((detail: RawMetricsDetail) => ({ increment: detail.increment, reason: detail.reason, line: detail.line + 1, // analyzers use 0-based; normalize to 1-based From 996bd2ae17a784bc86df5dc2dad35159655c3415 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:37:27 +0000 Subject: [PATCH 3/4] fix: guard showFunctionDetails against undefined func argument to fix test failure Agent-Logs-Url: https://github.com/askpt/code-metrics/sessions/bf3ddd58-ca9b-43db-8946-9201af04d92c Co-authored-by: askpt <2493377+askpt@users.noreply.github.com> --- src/extension.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/extension.ts b/src/extension.ts index aca70bb..69a183f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -12,9 +12,13 @@ let detailsChannel: vscode.OutputChannel | undefined; * shared output channel, then reveals the channel to the user. */ function showFunctionDetails( - func: UnifiedFunctionMetrics, + func?: UnifiedFunctionMetrics, _uri?: vscode.Uri ): void { + if (!func) { + return; + } + if (!detailsChannel) { detailsChannel = vscode.window.createOutputChannel("Code Metrics Details"); } From 268b22ae671b8f12ffd9596c09797917901e67d8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 1 Apr 2026 15:51:41 +0000 Subject: [PATCH 4/4] test: increase code coverage for extension.ts and metricsAnalyzerFactory.ts Agent-Logs-Url: https://github.com/askpt/code-metrics/sessions/1444435e-d531-4fbe-bb32-aec60a7765e5 Co-authored-by: askpt <2493377+askpt@users.noreply.github.com> --- src/metricsAnalyzer/metricsAnalyzerFactory.ts | 2 +- src/test/extension.test.ts | 71 +++++++++++++++++++ .../metricsAnalyzerFactory.test.ts | 27 +++++++ 3 files changed, 99 insertions(+), 1 deletion(-) diff --git a/src/metricsAnalyzer/metricsAnalyzerFactory.ts b/src/metricsAnalyzer/metricsAnalyzerFactory.ts index d7c2d98..0f40076 100644 --- a/src/metricsAnalyzer/metricsAnalyzerFactory.ts +++ b/src/metricsAnalyzer/metricsAnalyzerFactory.ts @@ -189,7 +189,7 @@ interface AnalyzerClass { * @returns A function that takes source text and returns an array of UnifiedFunctionMetrics * @throws {Error} If the module does not export the expected class with an `analyzeFile` method */ -function createAnalyzer( +export function createAnalyzer( modulePath: string, className: string ): (sourceText: string) => UnifiedFunctionMetrics[] { diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index 5e73189..4aee666 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -7,6 +7,7 @@ import * as assert from "assert"; import * as vscode from "vscode"; +import * as extensionModule from "../extension"; suite("Extension Activation Tests", () => { // Ensure extension is activated before running tests @@ -76,4 +77,74 @@ suite("Extension Activation Tests", () => { ); } }); + + test("should execute showFunctionDetails with non-empty details", async () => { + // Tests the table-rendering branch (details.length > 0) + const mockFunctionData: import("../metricsAnalyzer/metricsAnalyzerFactory").UnifiedFunctionMetrics = { + name: "ComplexFunction", + complexity: 3, + details: [ + { increment: 1, reason: "if statement", line: 5, column: 4, nesting: 0 }, + { increment: 2, reason: "nested if statement", line: 7, column: 8, nesting: 1 }, + ], + startLine: 3, + endLine: 15, + startColumn: 0, + endColumn: 1, + }; + + const mockUri = vscode.Uri.file("/test/complex.cs"); + + try { + await vscode.commands.executeCommand( + "cognitiveComplexity.showFunctionDetails", + mockFunctionData, + mockUri + ); + assert.ok(true, "Command with non-empty details executed without errors"); + } catch (error) { + assert.fail( + `Command execution with non-empty details should not throw errors, but got: ${error}` + ); + } + }); + + test("should reuse output channel when showFunctionDetails is called multiple times", async () => { + // Exercises the detailsChannel reuse path (second call skips createOutputChannel) + const mockFunctionData: import("../metricsAnalyzer/metricsAnalyzerFactory").UnifiedFunctionMetrics = { + name: "RepeatedFunction", + complexity: 1, + details: [ + { increment: 1, reason: "if statement", line: 2, column: 2, nesting: 0 }, + ], + startLine: 1, + endLine: 5, + startColumn: 0, + endColumn: 1, + }; + + try { + await vscode.commands.executeCommand( + "cognitiveComplexity.showFunctionDetails", + mockFunctionData + ); + // Second call should reuse the existing output channel + await vscode.commands.executeCommand( + "cognitiveComplexity.showFunctionDetails", + mockFunctionData + ); + assert.ok(true, "Channel reuse executed without errors"); + } catch (error) { + assert.fail( + `Channel reuse should not throw errors, but got: ${error}` + ); + } + }); + + test("should deactivate extension without errors", () => { + // Directly invoke deactivate to cover the disposal path + assert.doesNotThrow(() => { + extensionModule.deactivate(); + }, "deactivate() should not throw"); + }); }); diff --git a/src/test/metricsAnalyzer/metricsAnalyzerFactory.test.ts b/src/test/metricsAnalyzer/metricsAnalyzerFactory.test.ts index d07964f..822aabd 100644 --- a/src/test/metricsAnalyzer/metricsAnalyzerFactory.test.ts +++ b/src/test/metricsAnalyzer/metricsAnalyzerFactory.test.ts @@ -3,6 +3,7 @@ import { MetricsAnalyzerFactory, UnifiedFunctionMetrics, UnifiedMetricsDetail, + createAnalyzer, } from "../../metricsAnalyzer/metricsAnalyzerFactory"; suite("Metrics Analyzer Factory Tests", () => { @@ -851,4 +852,30 @@ func IsComplexCondition(value int, flag1, flag2 bool) bool { }); }); }); + + suite("createAnalyzer Runtime Guard", () => { + test("should throw a descriptive error when module does not export the expected class", () => { + // Use a valid module path but a class name that does not exist in it + const analyzer = createAnalyzer( + "./languages/csharpAnalyzer", + "NonExistentClass" + ); + + assert.throws( + () => analyzer("public class T {}"), + (err: unknown) => { + assert.ok(err instanceof Error); + assert.ok( + err.message.includes("NonExistentClass"), + `Expected error message to mention 'NonExistentClass', got: ${err.message}` + ); + assert.ok( + err.message.includes("analyzeFile"), + `Expected error message to mention 'analyzeFile', got: ${err.message}` + ); + return true; + } + ); + }); + }); });