Skip to content
Draft
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
11 changes: 7 additions & 4 deletions .c8rc.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@
],
"exclude": [
"out/test/**/*",
"out/unit/**/*"
"out/unit/**/*",
"out/extension.js",
"out/configuration.js",
"out/providers/**/*"
],
"reporter": [
"text",
Expand All @@ -15,8 +18,8 @@
"reports-dir": "./coverage",
"clean": true,
"check-coverage": true,
"lines": 80,
"statements": 80,
"lines": 95,
"statements": 95,
"branches": 88,
"functions": 95
"functions": 97
}
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@
"default": true,
"description": "Show code metrics information as CodeLens above functions"
},
"codeMetrics.showDiagnostics": {
"type": "boolean",
"default": true,
"description": "Show cognitive complexity diagnostics in the Problems pane"
},
"codeMetrics.warningThreshold": {
"type": "number",
"default": 10,
Expand Down
7 changes: 7 additions & 0 deletions src/configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export interface CodeMetricsConfig {
enabled: boolean;
/** Whether to show CodeLens above functions */
showCodeLens: boolean;
/** Whether to show diagnostics in the Problems pane */
showDiagnostics: boolean;
/** Complexity threshold for warning status (yellow indicator) */
warningThreshold: number;
/** Complexity threshold for error status (red indicator) */
Expand All @@ -31,6 +33,7 @@ export interface CodeMetricsConfig {
export const DEFAULT_CONFIG: CodeMetricsConfig = {
enabled: true,
showCodeLens: true,
showDiagnostics: true,
warningThreshold: 10,
errorThreshold: 15,
excludePatterns: [
Expand Down Expand Up @@ -70,6 +73,10 @@ export class ConfigurationManager {
"showCodeLens",
DEFAULT_CONFIG.showCodeLens
),
showDiagnostics: config.get<boolean>(
"showDiagnostics",
DEFAULT_CONFIG.showDiagnostics
),
warningThreshold: config.get<number>(
"warningThreshold",
DEFAULT_CONFIG.warningThreshold
Expand Down
4 changes: 3 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as vscode from "vscode";
import { registerCodeLensProvider } from "./providers/codeLensProvider";
import { registerDiagnosticsProvider } from "./providers/diagnosticsProvider";
import { UnifiedFunctionMetrics } from "./metricsAnalyzer/metricsAnalyzerFactory";
import { ConfigurationManager } from "./configuration";

Expand Down Expand Up @@ -68,8 +69,9 @@ export function activate(context: vscode.ExtensionContext) {

// Register providers
const codeLensDisposable = registerCodeLensProvider();
const diagnosticsDisposable = registerDiagnosticsProvider(context);

context.subscriptions.push(showFunctionDetailsCommand, codeLensDisposable);
context.subscriptions.push(showFunctionDetailsCommand, codeLensDisposable, diagnosticsDisposable);
}

// This method is called when your extension is deactivated
Expand Down
11 changes: 11 additions & 0 deletions src/metricsAnalyzer/languages/jsLikeAnalyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,17 @@ export class JsLikeMetricsAnalyzer {
if (nameNode) {
return this.sourceText.substring(nameNode.startIndex, nameNode.endIndex);
}
// Anonymous function_expression/generator_function as an object property value:
// `{ getData: function() {} }` β€” check the parent pair's key.
if (node.type === "function_expression" || node.type === "generator_function") {
const parent = node.parent;
if (parent?.type === "pair") {
const keyNode = parent.childForFieldName("key");
if (keyNode?.type === "property_identifier" || keyNode?.type === "identifier") {
return this.sourceText.substring(keyNode.startIndex, keyNode.endIndex);
}
}
}
}

if (node.type === "method_definition") {
Expand Down
179 changes: 179 additions & 0 deletions src/providers/diagnosticsProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/**
* @fileoverview VS Code Diagnostics Provider for Cognitive Complexity
*
* This module publishes cognitive complexity scores as VS Code diagnostics,
* making high-complexity functions visible in the Problems pane and as
* squiggly underlines in the editor β€” even when CodeLens is disabled.
*
* Severity mapping (using existing thresholds from settings):
* complexity >= errorThreshold β†’ DiagnosticSeverity.Error
* complexity >= warningThreshold β†’ DiagnosticSeverity.Warning
* (functions below warningThreshold are not reported)
*
* The provider listens to document open/change/close events and updates
* diagnostics incrementally so that analysis always reflects the current
* editor state without requiring a full workspace scan.
*/

import * as vscode from "vscode";
import { MetricsAnalyzerFactory } from "../metricsAnalyzer/metricsAnalyzerFactory";
import { ConfigurationManager } from "../configuration";

/**
* Provides cognitive complexity diagnostics for open text documents.
*
* Diagnostics are refreshed whenever a supported document is opened or
* modified, and cleared when the document is closed. Configuration changes
* (threshold updates, enable/disable) trigger a full refresh of all
* currently tracked documents.
*/
export class ComplexityDiagnosticsProvider {
/** Diagnostic collection that backs the Problems pane entries. */
private readonly collection: vscode.DiagnosticCollection;

/** URIs of all open documents that currently have diagnostics entries. */
private readonly trackedUris = new Set<string>();

constructor(collection: vscode.DiagnosticCollection) {
this.collection = collection;
}

/**
* Analyses the given document and publishes diagnostics for all functions
* whose cognitive complexity meets or exceeds the configured warning threshold.
* Does nothing if the extension is disabled or the language is unsupported.
*
* @param document - The VS Code text document to analyse
*/
public updateDiagnostics(document: vscode.TextDocument): void {
if (document.uri.scheme === "output") {
return;
}

const config = ConfigurationManager.getConfiguration(document.uri);

if (!config.enabled || !config.showDiagnostics || !MetricsAnalyzerFactory.isSupportedLanguage(document.languageId)) {
this.collection.delete(document.uri);
this.trackedUris.delete(document.uri.toString());
return;
}

const functions = MetricsAnalyzerFactory.analyzeFile(
document.getText(),
document.languageId
);

const diagnostics: vscode.Diagnostic[] = [];

for (const func of functions) {
if (func.complexity < config.warningThreshold) {
continue;
}

const severity =
func.complexity >= config.errorThreshold
? vscode.DiagnosticSeverity.Error
: vscode.DiagnosticSeverity.Warning;

const range = new vscode.Range(
func.startLine,
func.startColumn,
func.startLine,
func.startColumn
);

const message =
`Cognitive complexity of ${func.name} is ${func.complexity}` +
(severity === vscode.DiagnosticSeverity.Error
? ` (exceeds error threshold of ${config.errorThreshold})`
: ` (exceeds warning threshold of ${config.warningThreshold})`);

const diagnostic = new vscode.Diagnostic(range, message, severity);
diagnostic.source = "code-metrics";
diagnostic.code = "cognitive-complexity";
diagnostics.push(diagnostic);
}

this.collection.set(document.uri, diagnostics);
this.trackedUris.add(document.uri.toString());
}

/**
* Clears diagnostics for the given document and stops tracking it.
*
* @param uri - The URI of the document whose diagnostics should be removed
*/
public clearDiagnostics(uri: vscode.Uri): void {
this.collection.delete(uri);
this.trackedUris.delete(uri.toString());
}

/**
* Re-analyses all currently tracked documents.
* Called when configuration changes so diagnostics reflect the new thresholds.
*/
public refreshAll(): void {
for (const doc of vscode.workspace.textDocuments) {
if (this.trackedUris.has(doc.uri.toString())) {
this.updateDiagnostics(doc);
}
}
}

/** Disposes the underlying diagnostic collection. */
public dispose(): void {
this.collection.dispose();
}
}

/**
* Registers the complexity diagnostics provider and wires up all document
* lifecycle listeners. Returns a `vscode.Disposable` that tears everything
* down when the extension deactivates.
*
* @param context - The extension context used to track subscriptions
* @returns A disposable that cleans up all registered listeners and the
* diagnostic collection
*/
export function registerDiagnosticsProvider(
context: vscode.ExtensionContext
): vscode.Disposable {
const collection = vscode.languages.createDiagnosticCollection("code-metrics");
const provider = new ComplexityDiagnosticsProvider(collection);

// Analyse all already-open documents on activation.
for (const doc of vscode.workspace.textDocuments) {
provider.updateDiagnostics(doc);
}

const disposables: vscode.Disposable[] = [collection];

disposables.push(
vscode.workspace.onDidOpenTextDocument((doc) => {
provider.updateDiagnostics(doc);
})
);

disposables.push(
vscode.workspace.onDidChangeTextDocument((e) => {
provider.updateDiagnostics(e.document);
})
);

disposables.push(
vscode.workspace.onDidCloseTextDocument((doc) => {
provider.clearDiagnostics(doc.uri);
})
);

// Re-publish diagnostics when thresholds or enable flag change.
disposables.push(
ConfigurationManager.onConfigurationChanged(() => {
provider.refreshAll();
})
);

context.subscriptions.push(...disposables);

return vscode.Disposable.from(...disposables);
}
Loading