diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d69cba9..34b3451 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,6 +58,8 @@ jobs: # (lint is OS-agnostic and already runs on Linux via test:coverage) - run: npm run compile if: runner.os != 'Linux' + - run: npm run test:unit + if: runner.os != 'Linux' - run: npm run test:vscode if: runner.os != 'Linux' - name: Upload coverage to Codecov diff --git a/README.md b/README.md index 418b9f0..9ae4740 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ A Visual Studio Code extension that calculates and displays **Cognitive Complexi - **Real-time Analysis**: Analyzes code metrics as you write code - **CodeLens Integration**: Shows complexity scores directly above functions - **Color-coded Indicators**: Visual feedback with green/yellow/red status based on configurable thresholds -- **Multi-language Support**: Currently supports C#, Go, Java, JavaScript, JSX, Python, TypeScript, and TSX +- **Multi-language Support**: Currently supports C#, Go, Java, JavaScript, JSX, Python, Rust, TypeScript, and TSX - **Configurable Thresholds**: Customize warning and error complexity thresholds - **Smart Exclusions**: Automatically excludes test files, build artifacts, and other specified patterns @@ -23,6 +23,7 @@ A Visual Studio Code extension that calculates and displays **Cognitive Complexi | JavaScript | ✅ Supported | Full support including functions, methods, arrow functions, closures | | JSX | ✅ Supported | Full support for JavaScript with JSX syntax (React components) | | Python | ✅ Supported | Full support including functions, methods, lambdas, comprehensions, match statements | +| Rust | ✅ Supported | Full support including fn items, impl methods, if/for/while/loop/match expressions | | TypeScript | ✅ Supported | Full support including functions, methods, arrow functions, closures | | TSX | ✅ Supported | Full support for TypeScript with JSX syntax (React components) | diff --git a/package-lock.json b/package-lock.json index 0001bca..b518738 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "tree-sitter-java": "0.23.5", "tree-sitter-javascript": "0.21.4", "tree-sitter-python": "0.21.0", + "tree-sitter-rust": "^0.21.0", "tree-sitter-typescript": "0.21.2" }, "devDependencies": { @@ -6417,6 +6418,31 @@ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "license": "MIT" }, + "node_modules/tree-sitter-rust": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/tree-sitter-rust/-/tree-sitter-rust-0.21.0.tgz", + "integrity": "sha512-unVr73YLn3VC4Qa/GF0Nk+Wom6UtI526p5kz9Rn2iZSqwIFedyCZ3e0fKCEmUJLIPGrTb/cIEdu3ZUNGzfZx7A==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^7.1.0", + "node-gyp-build": "^4.8.0" + }, + "peerDependencies": { + "tree-sitter": "^0.21.1" + }, + "peerDependenciesMeta": { + "tree_sitter": { + "optional": true + } + } + }, + "node_modules/tree-sitter-rust/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/tree-sitter-typescript": { "version": "0.21.2", "resolved": "https://registry.npmjs.org/tree-sitter-typescript/-/tree-sitter-typescript-0.21.2.tgz", diff --git a/package.json b/package.json index d241072..183031d 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,8 @@ "golang", "metrics", "python", - "java" + "java", + "rust" ], "categories": [ "Other" @@ -44,7 +45,8 @@ "onLanguage:python", "onLanguage:typescript", "onLanguage:typescriptreact", - "onLanguage:java" + "onLanguage:java", + "onLanguage:rust" ], "main": "./out/extension.js", "contributes": { @@ -131,6 +133,7 @@ "tree-sitter-java": "0.23.5", "tree-sitter-javascript": "0.21.4", "tree-sitter-python": "0.21.0", + "tree-sitter-rust": "0.21.0", "tree-sitter-typescript": "0.21.2" } } diff --git a/samples/Test.rs b/samples/Test.rs new file mode 100644 index 0000000..152d534 --- /dev/null +++ b/samples/Test.rs @@ -0,0 +1,80 @@ +// Sample Rust file for testing Code Metrics extension +// This file demonstrates various Rust constructs that affect cognitive complexity. + +fn simple_function(x: i32) -> i32 { + x + 1 +} + +fn function_with_if(x: i32) -> i32 { + if x > 0 { + x + } else { + 0 + } +} + +fn function_with_loop(n: i32) -> i32 { + let mut sum = 0; + for i in 0..n { + sum += i; + } + sum +} + +fn function_with_match(x: i32) -> &'static str { + match x { + 0 => "zero", + 1..=9 => "single digit", + _ => "large", + } +} + +fn complex_function(x: i32, y: i32) -> i32 { + if x > 0 && y > 0 { + for i in 0..x { + if i % 2 == 0 || i == y { + return i; + } + } + } else if x < 0 { + let mut n = x; + while n < 0 { + n += 1; + } + return n; + } + 0 +} + +struct Calculator { + value: i32, +} + +impl Calculator { + fn new(value: i32) -> Self { + Calculator { value } + } + + fn apply(&self, op: &str, operand: i32) -> i32 { + match op { + "add" => self.value + operand, + "sub" => self.value - operand, + "mul" => self.value * operand, + _ => self.value, + } + } + + fn compute_series(&self, n: i32) -> i32 { + let mut result = self.value; + for i in 1..=n { + if i % 3 == 0 && i % 5 == 0 { + result += 15; + } else if i % 3 == 0 { + result += 3; + } else if i % 5 == 0 { + result += 5; + } + } + result + } +} diff --git a/src/metricsAnalyzer/languages/rustAnalyzer.ts b/src/metricsAnalyzer/languages/rustAnalyzer.ts new file mode 100644 index 0000000..9a867c7 --- /dev/null +++ b/src/metricsAnalyzer/languages/rustAnalyzer.ts @@ -0,0 +1,445 @@ +/** + * @fileoverview Rust Cognitive Complexity Analyzer + * + * This module provides cognitive complexity analysis for Rust source code using Tree-sitter. + * It implements the cognitive complexity metric which measures how difficult code is to + * understand, taking into account control flow, nesting, and other complexity factors. + * + * The analyzer uses the tree-sitter-rust parser to build an Abstract Syntax Tree (AST) + * and then traverses it to calculate complexity scores for each function/method. + */ + +import Parser from "tree-sitter"; +const Rust = require("tree-sitter-rust"); // noqa + +// Module-level singleton: parser initialization is expensive, so we reuse one instance per language. +const _parser = new Parser(); +_parser.setLanguage(Rust); + +/** + * Represents a single complexity detail for a specific Rust code construct. + * Each detail contributes to the overall cognitive complexity of a function. + */ +interface RustMetricsDetail { + /** The complexity increment this detail adds to the total complexity */ + increment: number; + /** Human-readable explanation of why this construct increases complexity */ + reason: string; + /** Line number where this complexity-contributing construct is located (0-based) */ + line: number; + /** Column number where this complexity-contributing construct starts (0-based) */ + column: number; + /** Current nesting level of this construct (0 for top-level) */ + nesting: number; +} + +/** + * Represents the complete cognitive complexity analysis results for a single Rust function or method. + * Includes the overall complexity score and detailed breakdown of contributing factors. + */ +interface RustFunctionMetrics { + /** The name or identifier of the function/method */ + name: string; + /** The total cognitive complexity score for this function */ + complexity: number; + /** Array of individual complexity details that contribute to the total score */ + details: RustMetricsDetail[]; + /** Line number where the function definition starts (0-based) */ + startLine: number; + /** Line number where the function definition ends (0-based) */ + endLine: number; + /** Column number where the function definition starts (0-based) */ + startColumn: number; + /** Column number where the function definition ends (0-based) */ + endColumn: number; +} + +/** + * Cognitive Complexity Analyzer for Rust source code. + * + * This class implements cognitive complexity analysis specifically for Rust code. + * Cognitive complexity is a metric that measures how difficult code is to understand, + * taking into account factors like: + * - Control flow statements (if, for, while, loop, match) + * - Nesting levels + * - Logical operators (&& and ||) + * - Closures (when nested) + * - Labeled breaks and continues + * + * The analyzer uses Tree-sitter for parsing and provides detailed analysis + * including the exact location and reason for each complexity increment. + * + * @example + * ```typescript + * const analyzer = new RustMetricsAnalyzer(); + * const results = analyzer.analyzeFunctions(rustSourceCode); + * console.log(`Function ${results[0].name} has complexity ${results[0].complexity}`); + * ``` + */ +export class RustMetricsAnalyzer { + /** Current nesting level during analysis */ + private nesting = 0; + /** Current complexity score during analysis */ + private complexity = 0; + /** Array of complexity details for the current function being analyzed */ + private details: RustMetricsDetail[] = []; + /** The source code text being analyzed */ + private sourceText: string; + /** Tree-sitter parser instance configured for Rust */ + private parser: Parser; + + /** + * Creates a new instance of the Rust cognitive complexity analyzer. + * Initializes the Tree-sitter parser with the Rust language grammar. + */ + constructor() { + this.parser = _parser; + this.sourceText = ""; + } + + /** + * Analyzes all functions in the provided Rust source code. + * + * This method parses the source code and identifies all function-like constructs + * (free functions and impl methods) and calculates their cognitive complexity scores. + * Nested function definitions are collected as separate entries and are not + * counted toward the enclosing function's complexity. + * + * @param sourceText - The complete Rust source code to analyze + * @returns An array of complexity analysis results, one for each function found + * + * @example + * ```typescript + * const sourceCode = ` + * fn add(a: i32, b: i32) -> i32 { + * if a < 0 || b < 0 { + * return 0; + * } + * a + b + * }`; + * const results = analyzer.analyzeFunctions(sourceCode); + * // results[0].complexity would be 2 (if statement + logical OR) + * ``` + */ + public analyzeFunctions(sourceText: string): RustFunctionMetrics[] { + this.sourceText = sourceText; + const tree = this.parser.parse(sourceText); + const functions: RustFunctionMetrics[] = []; + + const visit = (node: Parser.SyntaxNode) => { + if (this.isFunctionDeclaration(node)) { + const result = this.analyzeFunction(node); + if (result) { + functions.push(result); + } + const body = node.children.find((child) => child.type === "block"); + if (body) { + for (const child of body.children) { + visit(child); + } + } + } else { + for (const child of node.children) { + visit(child); + } + } + }; + + visit(tree.rootNode); + return functions; + } + + /** + * Determines if a syntax node represents a function declaration. + * + * @param node - The syntax node to check + * @returns True if the node represents a function item + */ + private isFunctionDeclaration(node: Parser.SyntaxNode): boolean { + return node.type === "function_item"; + } + + /** + * Determines the qualified name for a function, including impl type if applicable. + * + * @param node - The function_item syntax node + * @returns The qualified function name string + */ + private getFunctionName(node: Parser.SyntaxNode): string { + const nameNode = node.children.find((child) => child.type === "identifier"); + if (!nameNode) { + return ""; + } + const funcName = this.sourceText.substring( + nameNode.startIndex, + nameNode.endIndex + ); + + // Check if parent is a declaration_list within an impl_item + const parent = node.parent; + if (parent && parent.type === "declaration_list") { + const implNode = parent.parent; + if (implNode && implNode.type === "impl_item") { + const typeNode = implNode.children.find( + (child) => + child.type === "type_identifier" || + child.type === "generic_type" || + child.type === "scoped_type_identifier" + ); + if (typeNode) { + const typeName = this.sourceText.substring( + typeNode.startIndex, + typeNode.endIndex + ); + return `${typeName}::${funcName}`; + } + } + } + + return funcName; + } + + /** + * Analyzes the cognitive complexity of a single function. + * + * @param node - The syntax node representing the function item + * @returns Complexity analysis result or null if the function has no body + */ + private analyzeFunction(node: Parser.SyntaxNode): RustFunctionMetrics | null { + // Reset state for new function + this.nesting = 0; + this.complexity = 0; + this.details = []; + + const functionName = this.getFunctionName(node); + + // Find the function body (block node) + const body = node.children.find((child) => child.type === "block"); + if (!body) { + return null; + } + + this.visit(body); + + return { + name: functionName, + complexity: this.complexity, + details: [...this.details], + startLine: node.startPosition.row, + endLine: node.endPosition.row, + startColumn: node.startPosition.column, + endColumn: node.endPosition.column, + }; + } + + /** + * Recursively visits all nodes in the syntax tree to analyze complexity. + * + * @param node - The current syntax node being visited + * @param skipSelfIncrement - When true, skips this node's own structural increment + * (used for else-if nodes that are already counted by the enclosing else clause) + */ + private visit(node: Parser.SyntaxNode, skipSelfIncrement = false): void { + const baseIncrement = skipSelfIncrement ? 0 : this.getComplexityIncrement(node); + if (baseIncrement > 0) { + const nestingPenalty = this.getNestingPenalty(node); + const increment = baseIncrement + nestingPenalty; + const reason = this.getComplexityReason(node); + this.complexity += increment; + this.details.push({ + increment, + reason, + line: node.startPosition.row, + column: node.startPosition.column, + nesting: this.nesting, + }); + } + + if (this.increasesNesting(node)) { + this.nesting++; + for (const child of node.children) { + if (!this.isFunctionDeclaration(child)) { + if (this.shouldSkipChildStructuralIncrement(node, child)) { + this.visit(child, true); + continue; + } + this.visit(child); + } + } + this.nesting--; + } else { + for (const child of node.children) { + if (!this.isFunctionDeclaration(child)) { + if (this.shouldSkipChildStructuralIncrement(node, child)) { + this.visit(child, true); + continue; + } + this.visit(child); + } + } + } + } + + /** + * Returns the nesting penalty for a node's structural increment. + * Else/else-if clauses are counted as a flat +1 without nesting penalty. + */ + private getNestingPenalty(node: Parser.SyntaxNode): number { + return node.type === "else_clause" ? 0 : this.nesting; + } + + /** + * Determines whether a child node should skip its own structural increment. + * For else-if chains, the nested if_expression is already represented by the + * parent else_clause increment, so we avoid double-counting it. + */ + private shouldSkipChildStructuralIncrement( + parent: Parser.SyntaxNode, + child: Parser.SyntaxNode + ): boolean { + return parent.type === "else_clause" && child.type === "if_expression"; + } + + /** + * Calculates the complexity increment for a specific syntax node type. + * + * Based on cognitive complexity rules: + * - Control flow (if, for, while, loop, match): +1 + * - Else/else-if clauses: +1 (flat) + * - Logical operators (&& and ||): +1 each + * - Closures when nested: +1 + * - Labeled breaks/continues: +1 + * + * @param node - The syntax node to evaluate + * @returns The complexity increment (0 or positive integer) + */ + private getComplexityIncrement(node: Parser.SyntaxNode): number { + switch (node.type) { + case "if_expression": + case "for_expression": + case "while_expression": + case "loop_expression": + case "match_expression": + return 1; + case "else_clause": + return 1; + + case "binary_expression": { + const op = this.getBinaryOperator(node); + return op === "&&" || op === "||" ? 1 : 0; + } + + case "closure_expression": + return this.nesting > 0 ? 1 : 0; + + case "break_expression": + case "continue_expression": { + // Labeled break/continue always add complexity; unlabeled do not. + const hasLabel = this.hasLabel(node); + return hasLabel ? 1 : 0; + } + + default: + return 0; + } + } + + /** + * Extracts the binary operator from a binary expression node. + * + * @param node - The binary expression syntax node + * @returns The operator string or null if not found + */ + private getBinaryOperator(node: Parser.SyntaxNode): string | null { + for (const child of node.children) { + const text = this.sourceText.substring(child.startIndex, child.endIndex); + if (text === "&&" || text === "||") { + return text; + } + } + return null; + } + + /** + * Generates a human-readable reason for why a syntax node increases complexity. + * + * @param node - The syntax node that contributes to complexity + * @returns A descriptive string explaining the complexity increment + */ + private getComplexityReason(node: Parser.SyntaxNode): string { + switch (node.type) { + case "if_expression": + return "if expression"; + case "else_clause": { + const hasNestedIf = node.children.some((child) => child.type === "if_expression"); + return hasNestedIf ? "else if clause" : "else clause"; + } + case "for_expression": + return "for loop"; + case "while_expression": + return "while loop"; + case "loop_expression": + return "loop expression"; + case "match_expression": + return "match expression"; + case "binary_expression": { + const op = this.getBinaryOperator(node); + return `binary ${op} operator`; + } + case "closure_expression": + return "closure (nested)"; + case "break_expression": { + const hasLabel = this.hasLabel(node); + return hasLabel ? "labeled break" : "break (nested)"; + } + case "continue_expression": { + const hasLabel = this.hasLabel(node); + return hasLabel ? "labeled continue" : "continue (nested)"; + } + default: + return "unknown complexity source"; + } + } + + private hasLabel(node: Parser.SyntaxNode): boolean { + return node.children.some( + (child) => child.type === "label" || child.type === "loop_label" + ); + } + + /** + * Determines if a syntax node increases the nesting level. + * + * @param node - The syntax node to check + * @returns True if the node increases nesting level + */ + private increasesNesting(node: Parser.SyntaxNode): boolean { + return ( + node.type === "if_expression" || + node.type === "for_expression" || + node.type === "while_expression" || + node.type === "loop_expression" || + node.type === "match_expression" || + node.type === "closure_expression" + ); + } + + /** + * Static factory method to analyze Rust source code. + * + * @param sourceText - The complete Rust source code to analyze + * @returns An array of complexity analysis results for all functions found + * + * @example + * ```typescript + * const results = RustMetricsAnalyzer.analyzeFile(rustCode); + * results.forEach(func => { + * console.log(`${func.name}: ${func.complexity}`); + * }); + * ``` + */ + public static analyzeFile(sourceText: string): RustFunctionMetrics[] { + const analyzer = new RustMetricsAnalyzer(); + return analyzer.analyzeFunctions(sourceText); + } +} diff --git a/src/metricsAnalyzer/metricsAnalyzerFactory.ts b/src/metricsAnalyzer/metricsAnalyzerFactory.ts index f12029b..ececdb4 100644 --- a/src/metricsAnalyzer/metricsAnalyzerFactory.ts +++ b/src/metricsAnalyzer/metricsAnalyzerFactory.ts @@ -268,6 +268,7 @@ const languageAnalyzers: Record< python: createAnalyzer("./languages/pythonAnalyzer", "PythonMetricsAnalyzer"), typescript: createAnalyzer("./languages/typescriptAnalyzer", "TypeScriptMetricsAnalyzer"), typescriptreact: createAnalyzer("./languages/tsxAnalyzer", "TsxMetricsAnalyzer"), + rust: createAnalyzer("./languages/rustAnalyzer", "RustMetricsAnalyzer"), }; /** Set of supported language IDs for O(1) membership checks via {@link MetricsAnalyzerFactory.isSupportedLanguage}. */ diff --git a/src/unit/unit.test.ts b/src/unit/unit.test.ts index 8e62c6e..d2b62e8 100644 --- a/src/unit/unit.test.ts +++ b/src/unit/unit.test.ts @@ -13,6 +13,7 @@ import { JavaScriptMetricsAnalyzer } from "../metricsAnalyzer/languages/javascri import { PythonMetricsAnalyzer } from "../metricsAnalyzer/languages/pythonAnalyzer"; import { TsxMetricsAnalyzer } from "../metricsAnalyzer/languages/tsxAnalyzer"; import { TypeScriptMetricsAnalyzer } from "../metricsAnalyzer/languages/typescriptAnalyzer"; +import { RustMetricsAnalyzer } from "../metricsAnalyzer/languages/rustAnalyzer"; import { MetricsAnalyzerFactory, UnifiedFunctionMetrics, @@ -1009,7 +1010,7 @@ def greet(name): }); it("should return false for unsupported languages", () => { - const unsupported = ["ruby", "cpp", "rust", "swift", "kotlin", "php", ""]; + const unsupported = ["ruby", "cpp", "swift", "kotlin", "php", ""]; for (const lang of unsupported) { assert.strictEqual( MetricsAnalyzerFactory.isSupportedLanguage(lang), @@ -1036,6 +1037,7 @@ def greet(name): "javascript", "javascriptreact", "python", + "rust", "typescript", "typescriptreact", ]; @@ -1321,6 +1323,283 @@ function Layout({ children }: { children: React.ReactNode }) { }); }); + describe("Rust Analyzer Core Logic", () => { + it("should analyze a simple function with no complexity", () => { + const sourceCode = ` +fn add(a: i32, b: i32) -> i32 { + a + b +} +`; + const results = RustMetricsAnalyzer.analyzeFile(sourceCode); + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].name, "add"); + assert.strictEqual(results[0].complexity, 0); + }); + + it("should count if expression", () => { + const sourceCode = ` +fn check(x: i32) -> i32 { + if x > 0 { + 1 + } else { + 0 + } +} +`; + const results = RustMetricsAnalyzer.analyzeFile(sourceCode); + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].complexity, 2); + assert.deepStrictEqual( + results[0].details.map((d: UnifiedMetricsDetail) => d.reason), + ["if expression", "else clause"] + ); + assert.deepStrictEqual( + results[0].details.map((d: UnifiedMetricsDetail) => d.increment), + [1, 1] + ); + assert.deepStrictEqual( + results[0].details.map((d: UnifiedMetricsDetail) => d.nesting), + [0, 1] + ); + assert.strictEqual( + results[0].details[1].increment, + 1, + "else clause should be a flat +1 even when nested" + ); + }); + + it("should count for loop", () => { + const sourceCode = ` +fn sum(n: i32) -> i32 { + let mut s = 0; + for i in 0..n { + s += i; + } + s +} +`; + const results = RustMetricsAnalyzer.analyzeFile(sourceCode); + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].complexity, 1); + }); + + it("should count while loop", () => { + const sourceCode = ` +fn countdown(mut n: i32) { + while n > 0 { + n -= 1; + } +} +`; + const results = RustMetricsAnalyzer.analyzeFile(sourceCode); + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].complexity, 1); + }); + + it("should count loop expression", () => { + const sourceCode = ` +fn spin() { + loop { + break; + } +} +`; + const results = RustMetricsAnalyzer.analyzeFile(sourceCode); + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].complexity, 1); + }); + + it("should count match expression", () => { + const sourceCode = ` +fn classify(x: i32) -> &'static str { + match x { + 0 => "zero", + _ => "other", + } +} +`; + const results = RustMetricsAnalyzer.analyzeFile(sourceCode); + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].complexity, 1); + }); + + it("should count logical && and || operators", () => { + const sourceCode = ` +fn validate(a: i32, b: i32) -> bool { + a > 0 && b > 0 || a == b +} +`; + const results = RustMetricsAnalyzer.analyzeFile(sourceCode); + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].complexity, 2); + }); + + it("should count else-if chains without double-counting nested if expressions", () => { + const sourceCode = ` +fn classify(x: i32) -> i32 { + if x > 0 { + 1 + } else if x < 0 { + -1 + } else { + 0 + } +} +`; + const results = RustMetricsAnalyzer.analyzeFile(sourceCode); + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].complexity, 3); + assert.deepStrictEqual( + results[0].details.map((d: UnifiedMetricsDetail) => d.reason), + ["if expression", "else if clause", "else clause"] + ); + assert.deepStrictEqual( + results[0].details.map((d: UnifiedMetricsDetail) => d.increment), + [1, 1, 1] + ); + }); + + it("should apply nesting penalty for nested control flow", () => { + const sourceCode = ` +fn nested(x: i32) -> i32 { + if x > 0 { + for i in 0..x { + if i % 2 == 0 { + return i; + } + } + } + 0 +} +`; + const results = RustMetricsAnalyzer.analyzeFile(sourceCode); + assert.strictEqual(results.length, 1); + // outer if: +1 (nesting 0) = 1 + // for: +1 (nesting 1) + 1 = 2 + // inner if: +1 (nesting 2) + 2 = 3 + // total = 6 + assert.strictEqual(results[0].complexity, 6); + }); + + it("should count nested closures independently from enclosing control flow", () => { + const sourceCode = ` +fn wrap(flag: bool) { + if flag { + let _handler = || { + if flag { + 1 + } else { + 0 + } + }; + } +} +`; + const results = RustMetricsAnalyzer.analyzeFile(sourceCode); + assert.strictEqual(results.length, 1); + // outer if: +1 (nesting 0) = 1 + // closure: +1 (nesting 1) + 1 = 2 + // inner if: +1 (nesting 2) + 2 = 3 + // else clause: +1 (flat increment) = 1 + // total = 7 + assert.strictEqual(results[0].complexity, 7); + assert.deepStrictEqual( + results[0].details.map((d: UnifiedMetricsDetail) => d.reason), + ["if expression", "closure (nested)", "if expression", "else clause"] + ); + }); + + it("should qualify impl methods with type name", () => { + const sourceCode = ` +struct Counter { val: i32 } +impl Counter { + fn increment(&mut self) { + self.val += 1; + } +} +`; + const results = RustMetricsAnalyzer.analyzeFile(sourceCode); + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].name, "Counter::increment"); + }); + + it("should analyze nested functions as separate entries", () => { + const sourceCode = ` +fn outer() { + fn inner() { + if true { + } + } + + inner(); +} +`; + const results = RustMetricsAnalyzer.analyzeFile(sourceCode); + assert.strictEqual(results.length, 2); + assert.strictEqual(results[0].name, "outer"); + assert.strictEqual(results[0].complexity, 0); + assert.strictEqual(results[1].name, "inner"); + assert.strictEqual(results[1].complexity, 1); + }); + + it("should analyze multiple functions independently", () => { + const sourceCode = ` +fn simple(x: i32) -> i32 { x } +fn complex(x: i32) -> i32 { + if x > 0 { + x + } else { + 0 + } +} +`; + const results = RustMetricsAnalyzer.analyzeFile(sourceCode); + assert.strictEqual(results.length, 2); + assert.strictEqual(results[0].complexity, 0); + assert.strictEqual(results[1].complexity, 2); + }); + + it("should analyze Rust code via factory with rust language id", () => { + const sourceCode = ` +fn hello() -> i32 { + if true { 1 } else { 0 } +} +`; + const results = MetricsAnalyzerFactory.analyzeFile(sourceCode, "rust"); + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].complexity, 2); + }); + + it("should count labeled break and continue", () => { + const sourceCode = ` +fn loops() { + 'outer: loop { + if true { + break 'outer; + } + + 'inner: loop { + continue 'inner; + } + } +} +`; + const results = RustMetricsAnalyzer.analyzeFile(sourceCode); + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].complexity, 11); + assert.ok( + results[0].details.some( + (d: UnifiedMetricsDetail) => d.reason === "labeled break" + ) + ); + assert.ok( + results[0].details.some( + (d: UnifiedMetricsDetail) => d.reason === "labeled continue" + ) + ); + }); + }); + describe("Go Analyzer Additional Coverage", () => { it("should strip pointer dereference from pointer receiver method names", () => { const sourceCode = `