Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
57 changes: 51 additions & 6 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,62 @@ 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 (!func) {
return;
}

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 were reported.");
} 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
Expand All @@ -28,4 +71,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;
}
247 changes: 65 additions & 182 deletions src/metricsAnalyzer/metricsAnalyzerFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,163 +146,71 @@ 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.
*
* @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.
*
* @example
* ```typescript
* const analyzer = languageAnalyzers['csharp'];
* const complexityData = analyzer(sourceCode);
* ```
* Raw detail shape returned by all language-specific analyzers.
* `line` and `column` are 0-based and are normalized to 1-based by createAnalyzer.
*/
const languageAnalyzers: Record<
string,
(sourceText: string) => UnifiedFunctionMetrics[]
> = {
csharp: createCSharpAnalyzer(),
go: createGoAnalyzer(),
javascript: createJavaScriptAnalyzer(),
javascriptreact: createJavaScriptAnalyzer(),
typescript: createTypeScriptAnalyzer(),
typescriptreact: createTypeScriptAnalyzer(),
};
interface RawMetricsDetail {
increment: number;
reason: string;
line: number;
column: number;
nesting: number;
}

/**
* 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.
* 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.
*/
function createCSharpAnalyzer(): (
sourceText: string
) => UnifiedFunctionMetrics[] {
return function (sourceText: string) {
const { CSharpMetricsAnalyzer } = require("./languages/csharpAnalyzer");
const functions = CSharpMetricsAnalyzer.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
nesting: detail.nesting,
})),
startLine: func.startLine,
endLine: func.endLine,
startColumn: func.startColumn,
endColumn: func.endColumn,
}));
};
interface RawFunctionMetrics {
name: string;
complexity: number;
details: RawMetricsDetail[];
startLine: number;
endLine: number;
startColumn: number;
endColumn: number;
}

/**
* 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)
*
* @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,
}));
};
/** Shape of a language analyzer class that must expose a static `analyzeFile` method. */
interface AnalyzerClass {
analyzeFile(sourceText: string): RawFunctionMetrics[];
}

/**
* Creates a JavaScript cognitive complexity analyzer function.
* Creates a language-specific cognitive complexity analyzer function.
*
* 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.
*
* @returns A function that analyzes JavaScript source code and returns an array of function complexity metrics.
* @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 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;
export function createAnalyzer(
modulePath: string,
className: string
): (sourceText: string) => UnifiedFunctionMetrics[] {
return function (sourceText: string): UnifiedFunctionMetrics[] {
const mod = require(modulePath) as Record<string, AnalyzerClass | undefined>;
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 = JavaScriptMetricsAnalyzer.analyzeFile(sourceText) as JSFunctionMetrics[];
return functions.map((func: JSFunctionMetrics) => ({
const functions: RawFunctionMetrics[] = analyzerClass.analyzeFile(sourceText);
return functions.map((func: RawFunctionMetrics) => ({
name: func.name,
complexity: func.complexity,
details: func.details.map((detail: JSDetail) => ({
details: func.details.map((detail: RawMetricsDetail) => ({
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
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,
Expand All @@ -314,46 +222,21 @@ function createJavaScriptAnalyzer(): (
}

/**
* Creates a TypeScript cognitive complexity analyzer function.
* A record of language-specific analyzers that compute cognitive complexity metrics for source code.
*
* @returns A function that analyzes TypeScript source code and returns an array of function complexity metrics.
* @remarks
* 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"),
};
Loading
Loading