From edda550cae7fe49bfe715b084492661e22597b14 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 21 Apr 2026 22:20:37 +0000 Subject: [PATCH 1/3] refactor: extract shared base class for JavaScript and TypeScript analyzers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The JavaScriptMetricsAnalyzer and TypeScriptMetricsAnalyzer classes were nearly identical (~400 lines of duplicated logic). Extract the common analysis algorithm into a new JsLikeMetricsAnalyzer base class in jsLikeAnalyzer.ts. Both analyzers now extend this base, providing only the language-specific Tree-sitter parser instance. No behaviour changes — the public API and analysis output are identical. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../languages/javascriptAnalyzer.ts | 426 +----------------- .../languages/jsLikeAnalyzer.ts | 418 +++++++++++++++++ .../languages/typescriptAnalyzer.ts | 421 +---------------- 3 files changed, 444 insertions(+), 821 deletions(-) create mode 100644 src/metricsAnalyzer/languages/jsLikeAnalyzer.ts diff --git a/src/metricsAnalyzer/languages/javascriptAnalyzer.ts b/src/metricsAnalyzer/languages/javascriptAnalyzer.ts index 7a780a3..00d94c0 100644 --- a/src/metricsAnalyzer/languages/javascriptAnalyzer.ts +++ b/src/metricsAnalyzer/languages/javascriptAnalyzer.ts @@ -2,73 +2,26 @@ * @fileoverview JavaScript Cognitive Complexity Analyzer * * This module provides cognitive complexity analysis for JavaScript 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 analysis logic is shared with the TypeScript analyzer via {@link JsLikeMetricsAnalyzer}. + * This class sets up the JavaScript parser and delegates all analysis to the base class. * - * The analyzer uses the tree-sitter-javascript parser to build an Abstract Syntax Tree (AST) - * and then traverses it to calculate complexity scores for each function/method. + * @see JsLikeMetricsAnalyzer for the full list of constructs that affect complexity. */ import Parser from "tree-sitter"; +import { JsLikeMetricsAnalyzer, JsLikeFunctionMetrics } from "./jsLikeAnalyzer"; + const JavaScript = require("tree-sitter-javascript"); // noqa // Module-level singleton: parser initialization is expensive, so we reuse one instance per language. const _parser = new Parser(); _parser.setLanguage(JavaScript); -/** - * Represents a single complexity detail for a specific JavaScript code construct. - * Each detail contributes to the overall cognitive complexity of a function. - */ -interface JavaScriptMetricsDetail { - /** 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 JavaScript function. - * Includes the overall complexity score and detailed breakdown of contributing factors. - */ -interface JavaScriptFunctionMetrics { - /** 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: JavaScriptMetricsDetail[]; - /** 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 JavaScript source code. * - * This class implements cognitive complexity analysis specifically for JavaScript 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, do, switch) - * - Nesting levels - * - Catch clauses (exception handling) - * - Logical operators (&&, ||, ??) - * - Ternary expressions - * - Nested functions and arrow functions - * - * The analyzer uses Tree-sitter for parsing and provides detailed analysis - * including the exact location and reason for each complexity increment. + * Delegates all analysis logic to {@link JsLikeMetricsAnalyzer}; this class only + * provides the JavaScript-specific Tree-sitter parser. * * @example * ```typescript @@ -76,357 +29,9 @@ interface JavaScriptFunctionMetrics { * console.log(`Function ${results[0].name} has complexity ${results[0].complexity}`); * ``` */ -export class JavaScriptMetricsAnalyzer { - /** 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: JavaScriptMetricsDetail[] = []; - /** The source code text being analyzed */ - private sourceText: string; - /** Tree-sitter parser instance configured for JavaScript */ - private parser: Parser; - - /** - * Creates a new instance of the JavaScript cognitive complexity analyzer. - * Initializes the Tree-sitter parser with the JavaScript language grammar. - */ +export class JavaScriptMetricsAnalyzer extends JsLikeMetricsAnalyzer { constructor() { - this.parser = _parser; - this.sourceText = ""; - } - - /** - * Analyzes all functions in the provided JavaScript source code. - * - * @param sourceText - The complete JavaScript source code to analyze - * @returns An array of complexity analysis results, one for each function found - */ - public analyzeFunctions(sourceText: string): JavaScriptFunctionMetrics[] { - this.sourceText = sourceText; - const tree = this.parser.parse(sourceText); - const functions: JavaScriptFunctionMetrics[] = []; - this.collectFunctions(tree.rootNode, functions, false); - return functions; - } - - /** - * Recursively collects functions from the AST and calculates their complexity. - * - * @param node - The current AST node to process - * @param functions - The array to accumulate function metrics into - * @param isNested - Whether this function is nested inside another function - */ - private collectFunctions( - node: Parser.SyntaxNode, - functions: JavaScriptFunctionMetrics[], - isNested: boolean - ): void { - const isFunctionNode = - node.type === "function_declaration" || - node.type === "function_expression" || - node.type === "method_definition" || - node.type === "arrow_function"; - - if (isFunctionNode) { - // If nested, add complexity for the nesting - if (isNested) { - this.complexity += 1 + this.nesting; - this.details.push({ - increment: 1 + this.nesting, - reason: this.getFunctionReason(node.type), - line: node.startPosition.row, - column: node.startPosition.column, - nesting: this.nesting, - }); - } - - // Save current state before analyzing nested function - const savedComplexity = this.complexity; - const savedDetails = this.details; - const savedNesting = this.nesting; - - // Reset for the new function - this.complexity = 0; - this.details = []; - this.nesting = 0; - - // Analyze the function body - const funcName = this.getFunctionName(node); - this.analyzeNode(node); - - const metrics: JavaScriptFunctionMetrics = { - name: funcName, - complexity: this.complexity, - details: this.details, - startLine: node.startPosition.row, - endLine: node.endPosition.row, - startColumn: node.startPosition.column, - endColumn: node.endPosition.column, - }; - functions.push(metrics); - - // Restore state - this.complexity = savedComplexity; - this.details = savedDetails; - this.nesting = savedNesting; - } else { - for (const child of node.children) { - this.collectFunctions(child, functions, isNested); - } - } - } - - /** - * Returns a human-readable reason for a function node type. - */ - private getFunctionReason(nodeType: string): string { - switch (nodeType) { - case "arrow_function": - return "arrow function (nested)"; - case "function_expression": - return "function expression (nested)"; - case "method_definition": - return "method (nested)"; - default: - return "function (nested)"; - } - } - - /** - * Extracts the name of a function from its AST node. - * - * @param node - The function AST node - * @returns The function name or a descriptive placeholder - */ - private getFunctionName(node: Parser.SyntaxNode): string { - if (node.type === "function_declaration" || node.type === "function_expression") { - const nameNode = node.children.find((c) => c.type === "identifier"); - if (nameNode) { - return this.sourceText.substring(nameNode.startIndex, nameNode.endIndex); - } - } - - if (node.type === "method_definition") { - const nameNode = node.children.find( - (c) => c.type === "property_identifier" || c.type === "identifier" - ); - if (nameNode) { - return this.sourceText.substring(nameNode.startIndex, nameNode.endIndex); - } - } - - if (node.type === "arrow_function") { - // Arrow functions may be assigned to a variable - check parent - const parent = node.parent; - if (parent && parent.type === "variable_declarator") { - const nameNode = parent.children.find((c) => c.type === "identifier"); - if (nameNode) { - return this.sourceText.substring(nameNode.startIndex, nameNode.endIndex); - } - } - return "(arrow function)"; - } - - return "(anonymous)"; - } - - /** - * Analyzes a single AST node and its children for complexity contributions. - * - * @param node - The AST node to analyze - * @param skipSelfIncrement - When true, skip incrementing complexity for this node (used for else-if) - */ - private analyzeNode(node: Parser.SyntaxNode, skipSelfIncrement = false): void { - if (!skipSelfIncrement) { - const increment = this.getComplexityIncrement(node); - if (increment > 0) { - this.details.push({ - increment, - reason: this.getComplexityReason(node), - line: node.startPosition.row, - column: node.startPosition.column, - nesting: this.nesting, - }); - this.complexity += increment; - } - } - - const nestingIncreased = this.increasesNesting(node); - if (nestingIncreased) { - this.nesting++; - } - - for (const child of node.children) { - // Skip nested function bodies - they are analyzed separately - if (this.isNestedFunction(child)) { - this.collectFunctions(child, [], true); - continue; - } - // For else_clause containing if_statement (else-if): - // The else_clause is already counted above. The inner if_statement's structural - // increment is skipped to avoid double-counting, but its body is still analyzed. - if (node.type === "else_clause" && child.type === "if_statement") { - this.analyzeNode(child, true); - continue; - } - this.analyzeNode(child); - } - - if (nestingIncreased) { - this.nesting--; - } - } - - /** - * Checks if a node is a nested function definition. - */ - private isNestedFunction(node: Parser.SyntaxNode): boolean { - return ( - node.type === "function_expression" || - node.type === "arrow_function" || - node.type === "method_definition" - ); - } - - /** - * Calculates the complexity increment for a specific syntax node type. - * - * Based on cognitive complexity rules: - * - Control flow statements: +1 (+ nesting level) - * - Else/else-if clauses: +1 (flat, no nesting) - * - Catch clauses: +1 - * - Ternary expressions: +1 - * - Logical operators (&&, ||, ??): +1 each - * - Labeled break/continue: +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) { - // Control flow statements (+1 + nesting) - case "if_statement": - case "for_statement": - case "for_in_statement": - case "for_of_statement": - case "while_statement": - case "do_statement": - case "switch_statement": - return 1 + this.nesting; - - // Else/else-if clauses (+1, flat) - case "else_clause": - return 1; - - // Exception handling (+1) - case "catch_clause": - return 1; - - // Ternary expressions (+1) - case "ternary_expression": - return 1; - - // Logical operators (+1 each) - case "binary_expression": - case "logical_expression": { - const op = this.getOperator(node); - if (op === "&&" || op === "||" || op === "??") { - return 1; - } - return 0; - } - - // Labeled break/continue (+1) - case "break_statement": - case "continue_statement": { - const hasLabel = node.children.some( - (c) => c.type === "statement_identifier" - ); - return hasLabel ? 1 : 0; - } - - default: - return 0; - } - } - - /** - * Extracts the operator from a binary or logical expression node. - */ - private getOperator(node: Parser.SyntaxNode): string | null { - for (const child of node.children) { - const text = this.sourceText.substring(child.startIndex, child.endIndex); - if (text === "&&" || 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_statement": - return "if statement"; - case "else_clause": { - // Distinguish between else and else-if - const hasNestedIf = node.children.some((c) => c.type === "if_statement"); - return hasNestedIf ? "else if clause" : "else clause"; - } - case "for_statement": - return "for loop"; - case "for_in_statement": - return "for...in loop"; - case "for_of_statement": - return "for...of loop"; - case "while_statement": - return "while loop"; - case "do_statement": - return "do...while loop"; - case "switch_statement": - return "switch statement"; - case "catch_clause": - return "catch clause"; - case "ternary_expression": - return "ternary expression"; - case "binary_expression": - case "logical_expression": { - const op = this.getOperator(node); - return `logical ${op} operator`; - } - case "break_statement": - return "labeled break statement"; - case "continue_statement": - return "labeled continue statement"; - default: - return "complexity source"; - } - } - - /** - * 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_statement" || - node.type === "for_statement" || - node.type === "for_in_statement" || - node.type === "for_of_statement" || - node.type === "while_statement" || - node.type === "do_statement" || - node.type === "switch_statement" || - node.type === "catch_clause" - ); + super(_parser); } /** @@ -434,17 +39,8 @@ export class JavaScriptMetricsAnalyzer { * * @param sourceText - The complete JavaScript source code to analyze * @returns An array of complexity analysis results for all functions found - * - * @example - * ```typescript - * const results = JavaScriptMetricsAnalyzer.analyzeFile(jsCode); - * results.forEach(func => { - * console.log(`${func.name}: ${func.complexity}`); - * }); - * ``` */ - public static analyzeFile(sourceText: string): JavaScriptFunctionMetrics[] { - const analyzer = new JavaScriptMetricsAnalyzer(); - return analyzer.analyzeFunctions(sourceText); + public static analyzeFile(sourceText: string): JsLikeFunctionMetrics[] { + return new JavaScriptMetricsAnalyzer().analyzeFunctions(sourceText); } } diff --git a/src/metricsAnalyzer/languages/jsLikeAnalyzer.ts b/src/metricsAnalyzer/languages/jsLikeAnalyzer.ts new file mode 100644 index 0000000..d097394 --- /dev/null +++ b/src/metricsAnalyzer/languages/jsLikeAnalyzer.ts @@ -0,0 +1,418 @@ +/** + * @fileoverview Shared base class for JavaScript and TypeScript Cognitive Complexity Analyzers + * + * This module provides the common analysis logic shared by the JavaScript and TypeScript + * cognitive complexity analyzers. Both languages share identical cognitive complexity + * constructs, so the analysis algorithm is extracted here to avoid duplication. + * + * Subclasses provide only the language-specific Tree-sitter parser instance. + */ + +import Parser from "tree-sitter"; + +/** + * Represents a single complexity detail for a specific JS/TS code construct. + * Each detail contributes to the overall cognitive complexity of a function. + */ +export interface JsLikeMetricsDetail { + /** 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 JS/TS function. + * Includes the overall complexity score and detailed breakdown of contributing factors. + */ +export interface JsLikeFunctionMetrics { + /** 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: JsLikeMetricsDetail[]; + /** 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; +} + +/** + * Shared base class implementing cognitive complexity analysis for JavaScript-like languages. + * + * Cognitive complexity factors handled: + * - Control flow statements (if, for, while, do, switch): +1 + nesting level + * - Else/else-if clauses: +1 (flat, no nesting penalty) + * - Catch clauses (exception handling): +1 + * - Ternary expressions: +1 + * - Logical operators (&&, ||, ??): +1 each + * - Nested functions and arrow functions: +1 + nesting level + * - Labeled break/continue: +1 + * + * Subclasses configure the Tree-sitter parser for the target language by passing + * a pre-initialised `Parser` instance to the constructor. + */ +export class JsLikeMetricsAnalyzer { + /** 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: JsLikeMetricsDetail[] = []; + /** The source code text being analyzed */ + private sourceText: string; + /** Tree-sitter parser instance configured for the target language */ + private parser: Parser; + + /** + * Creates a new instance of the JS-like cognitive complexity analyzer. + * + * @param parser - A Tree-sitter `Parser` instance already configured with the target language grammar + */ + constructor(parser: Parser) { + this.parser = parser; + this.sourceText = ""; + } + + /** + * Analyzes all functions in the provided source code. + * + * @param sourceText - The complete source code to analyze + * @returns An array of complexity analysis results, one for each function found + */ + public analyzeFunctions(sourceText: string): JsLikeFunctionMetrics[] { + this.sourceText = sourceText; + const tree = this.parser.parse(sourceText); + const functions: JsLikeFunctionMetrics[] = []; + this.collectFunctions(tree.rootNode, functions, false); + return functions; + } + + /** + * Recursively collects functions from the AST and calculates their complexity. + * + * @param node - The current AST node to process + * @param functions - The array to accumulate function metrics into + * @param isNested - Whether this function is nested inside another function + */ + private collectFunctions( + node: Parser.SyntaxNode, + functions: JsLikeFunctionMetrics[], + isNested: boolean + ): void { + const isFunctionNode = + node.type === "function_declaration" || + node.type === "function_expression" || + node.type === "method_definition" || + node.type === "arrow_function"; + + if (isFunctionNode) { + // If nested, add complexity for the nesting + if (isNested) { + this.complexity += 1 + this.nesting; + this.details.push({ + increment: 1 + this.nesting, + reason: this.getFunctionReason(node.type), + line: node.startPosition.row, + column: node.startPosition.column, + nesting: this.nesting, + }); + } + + // Save current state before analyzing nested function + const savedComplexity = this.complexity; + const savedDetails = this.details; + const savedNesting = this.nesting; + + // Reset for the new function + this.complexity = 0; + this.details = []; + this.nesting = 0; + + // Analyze the function body + const funcName = this.getFunctionName(node); + this.analyzeNode(node); + + const metrics: JsLikeFunctionMetrics = { + name: funcName, + complexity: this.complexity, + details: this.details, + startLine: node.startPosition.row, + endLine: node.endPosition.row, + startColumn: node.startPosition.column, + endColumn: node.endPosition.column, + }; + functions.push(metrics); + + // Restore state + this.complexity = savedComplexity; + this.details = savedDetails; + this.nesting = savedNesting; + } else { + for (const child of node.children) { + this.collectFunctions(child, functions, isNested); + } + } + } + + /** + * Returns a human-readable reason for a function node type. + */ + private getFunctionReason(nodeType: string): string { + switch (nodeType) { + case "arrow_function": + return "arrow function (nested)"; + case "function_expression": + return "function expression (nested)"; + case "method_definition": + return "method (nested)"; + default: + return "function (nested)"; + } + } + + /** + * Extracts the name of a function from its AST node. + * + * @param node - The function AST node + * @returns The function name or a descriptive placeholder + */ + private getFunctionName(node: Parser.SyntaxNode): string { + if (node.type === "function_declaration" || node.type === "function_expression") { + const nameNode = node.children.find((c) => c.type === "identifier"); + if (nameNode) { + return this.sourceText.substring(nameNode.startIndex, nameNode.endIndex); + } + } + + if (node.type === "method_definition") { + const nameNode = node.children.find( + (c) => c.type === "property_identifier" || c.type === "identifier" + ); + if (nameNode) { + return this.sourceText.substring(nameNode.startIndex, nameNode.endIndex); + } + } + + if (node.type === "arrow_function") { + // Arrow functions may be assigned to a variable - check parent + const parent = node.parent; + if (parent && parent.type === "variable_declarator") { + const nameNode = parent.children.find((c) => c.type === "identifier"); + if (nameNode) { + return this.sourceText.substring(nameNode.startIndex, nameNode.endIndex); + } + } + return "(arrow function)"; + } + + return "(anonymous)"; + } + + /** + * Analyzes a single AST node and its children for complexity contributions. + * + * @param node - The AST node to analyze + * @param skipSelfIncrement - When true, skip incrementing complexity for this node (used for else-if) + */ + private analyzeNode(node: Parser.SyntaxNode, skipSelfIncrement = false): void { + if (!skipSelfIncrement) { + const increment = this.getComplexityIncrement(node); + if (increment > 0) { + this.details.push({ + increment, + reason: this.getComplexityReason(node), + line: node.startPosition.row, + column: node.startPosition.column, + nesting: this.nesting, + }); + this.complexity += increment; + } + } + + const nestingIncreased = this.increasesNesting(node); + if (nestingIncreased) { + this.nesting++; + } + + for (const child of node.children) { + // Skip nested function bodies - they are analyzed separately + if (this.isNestedFunction(child)) { + this.collectFunctions(child, [], true); + continue; + } + // For else_clause containing if_statement (else-if): + // The else_clause is already counted above. The inner if_statement's structural + // increment is skipped to avoid double-counting, but its body is still analyzed. + if (node.type === "else_clause" && child.type === "if_statement") { + this.analyzeNode(child, true); + continue; + } + this.analyzeNode(child); + } + + if (nestingIncreased) { + this.nesting--; + } + } + + /** + * Checks if a node is a nested function definition. + */ + private isNestedFunction(node: Parser.SyntaxNode): boolean { + return ( + node.type === "function_expression" || + node.type === "arrow_function" || + node.type === "method_definition" + ); + } + + /** + * Calculates the complexity increment for a specific syntax node type. + * + * Based on cognitive complexity rules: + * - Control flow statements: +1 + nesting level + * - Else/else-if clauses: +1 (flat) + * - Catch clauses: +1 + * - Ternary expressions: +1 + * - Logical operators (&&, ||, ??): +1 each + * - Labeled break/continue: +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) { + // Control flow statements (+1 + nesting) + case "if_statement": + case "for_statement": + case "for_in_statement": + case "for_of_statement": + case "while_statement": + case "do_statement": + case "switch_statement": + return 1 + this.nesting; + + // Else/else-if clauses (+1, flat) + case "else_clause": + return 1; + + // Exception handling (+1) + case "catch_clause": + return 1; + + // Ternary expressions (+1) + case "ternary_expression": + return 1; + + // Logical operators (+1 each) + case "binary_expression": + case "logical_expression": { + const op = this.getOperator(node); + if (op === "&&" || op === "||" || op === "??") { + return 1; + } + return 0; + } + + // Labeled break/continue (+1) + case "break_statement": + case "continue_statement": { + const hasLabel = node.children.some( + (c) => c.type === "statement_identifier" + ); + return hasLabel ? 1 : 0; + } + + default: + return 0; + } + } + + /** + * Extracts the operator from a binary or logical expression node. + */ + private getOperator(node: Parser.SyntaxNode): string | null { + for (const child of node.children) { + const text = this.sourceText.substring(child.startIndex, child.endIndex); + if (text === "&&" || 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_statement": + return "if statement"; + case "else_clause": { + const hasNestedIf = node.children.some((c) => c.type === "if_statement"); + return hasNestedIf ? "else if clause" : "else clause"; + } + case "for_statement": + return "for loop"; + case "for_in_statement": + return "for...in loop"; + case "for_of_statement": + return "for...of loop"; + case "while_statement": + return "while loop"; + case "do_statement": + return "do...while loop"; + case "switch_statement": + return "switch statement"; + case "catch_clause": + return "catch clause"; + case "ternary_expression": + return "ternary expression"; + case "binary_expression": + case "logical_expression": { + const op = this.getOperator(node); + return `logical ${op} operator`; + } + case "break_statement": + return "labeled break statement"; + case "continue_statement": + return "labeled continue statement"; + default: + return "complexity source"; + } + } + + /** + * 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_statement" || + node.type === "for_statement" || + node.type === "for_in_statement" || + node.type === "for_of_statement" || + node.type === "while_statement" || + node.type === "do_statement" || + node.type === "switch_statement" || + node.type === "catch_clause" + ); + } +} diff --git a/src/metricsAnalyzer/languages/typescriptAnalyzer.ts b/src/metricsAnalyzer/languages/typescriptAnalyzer.ts index f234077..8ff8040 100644 --- a/src/metricsAnalyzer/languages/typescriptAnalyzer.ts +++ b/src/metricsAnalyzer/languages/typescriptAnalyzer.ts @@ -2,74 +2,30 @@ * @fileoverview TypeScript Cognitive Complexity Analyzer * * This module provides cognitive complexity analysis for TypeScript 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 analysis logic is shared with the JavaScript analyzer via {@link JsLikeMetricsAnalyzer}. + * This class sets up the TypeScript parser and delegates all analysis to the base class. * - * TypeScript is a superset of JavaScript, so this analyzer handles the same constructs - * as the JavaScript analyzer plus TypeScript-specific syntax. It uses the tree-sitter-typescript - * parser for accurate AST generation. + * TypeScript is a superset of JavaScript, so it handles the same cognitive complexity + * constructs as JavaScript. TypeScript-specific syntax (type annotations, interfaces, + * generics) generally does not affect cognitive complexity. + * + * @see JsLikeMetricsAnalyzer for the full list of constructs that affect complexity. */ import Parser from "tree-sitter"; +import { JsLikeMetricsAnalyzer, JsLikeFunctionMetrics } from "./jsLikeAnalyzer"; + const { typescript: TypeScriptLanguage } = require("tree-sitter-typescript"); // noqa // Module-level singleton: parser initialization is expensive, so we reuse one instance per language. const _parser = new Parser(); _parser.setLanguage(TypeScriptLanguage); -/** - * Represents a single complexity detail for a specific TypeScript code construct. - * Each detail contributes to the overall cognitive complexity of a function. - */ -interface TypeScriptMetricsDetail { - /** 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 TypeScript function. - * Includes the overall complexity score and detailed breakdown of contributing factors. - */ -interface TypeScriptFunctionMetrics { - /** 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: TypeScriptMetricsDetail[]; - /** 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 TypeScript source code. * - * This class implements cognitive complexity analysis specifically for TypeScript code. - * Since TypeScript is a superset of JavaScript, it handles the same control flow constructs - * as the JavaScript analyzer. TypeScript-specific syntax (type annotations, interfaces, - * generics) generally does not affect cognitive complexity. - * - * Cognitive complexity factors include: - * - Control flow statements (if, for, while, do, switch) - * - Nesting levels - * - Catch clauses (exception handling) - * - Logical operators (&&, ||, ??) - * - Ternary expressions - * - Nested functions and arrow functions + * Delegates all analysis logic to {@link JsLikeMetricsAnalyzer}; this class only + * provides the TypeScript-specific Tree-sitter parser. * * @example * ```typescript @@ -77,347 +33,9 @@ interface TypeScriptFunctionMetrics { * console.log(`Function ${results[0].name} has complexity ${results[0].complexity}`); * ``` */ -export class TypeScriptMetricsAnalyzer { - /** 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: TypeScriptMetricsDetail[] = []; - /** The source code text being analyzed */ - private sourceText: string; - /** Tree-sitter parser instance configured for TypeScript */ - private parser: Parser; - - /** - * Creates a new instance of the TypeScript cognitive complexity analyzer. - * Initializes the Tree-sitter parser with the TypeScript language grammar. - */ +export class TypeScriptMetricsAnalyzer extends JsLikeMetricsAnalyzer { constructor() { - this.parser = _parser; - this.sourceText = ""; - } - - /** - * Analyzes all functions in the provided TypeScript source code. - * - * @param sourceText - The complete TypeScript source code to analyze - * @returns An array of complexity analysis results, one for each function found - */ - public analyzeFunctions(sourceText: string): TypeScriptFunctionMetrics[] { - this.sourceText = sourceText; - const tree = this.parser.parse(sourceText); - const functions: TypeScriptFunctionMetrics[] = []; - this.collectFunctions(tree.rootNode, functions, false); - return functions; - } - - /** - * Recursively collects functions from the AST and calculates their complexity. - * - * @param node - The current AST node to process - * @param functions - The array to accumulate function metrics into - * @param isNested - Whether this function is nested inside another function - */ - private collectFunctions( - node: Parser.SyntaxNode, - functions: TypeScriptFunctionMetrics[], - isNested: boolean - ): void { - const isFunctionNode = - node.type === "function_declaration" || - node.type === "function_expression" || - node.type === "method_definition" || - node.type === "arrow_function"; - - if (isFunctionNode) { - // If nested, add complexity for the nesting - if (isNested) { - this.complexity += 1 + this.nesting; - this.details.push({ - increment: 1 + this.nesting, - reason: this.getFunctionReason(node.type), - line: node.startPosition.row, - column: node.startPosition.column, - nesting: this.nesting, - }); - } - - // Save current state before analyzing nested function - const savedComplexity = this.complexity; - const savedDetails = this.details; - const savedNesting = this.nesting; - - // Reset for the new function - this.complexity = 0; - this.details = []; - this.nesting = 0; - - // Analyze the function body - const funcName = this.getFunctionName(node); - this.analyzeNode(node); - - const metrics: TypeScriptFunctionMetrics = { - name: funcName, - complexity: this.complexity, - details: this.details, - startLine: node.startPosition.row, - endLine: node.endPosition.row, - startColumn: node.startPosition.column, - endColumn: node.endPosition.column, - }; - functions.push(metrics); - - // Restore state - this.complexity = savedComplexity; - this.details = savedDetails; - this.nesting = savedNesting; - } else { - for (const child of node.children) { - this.collectFunctions(child, functions, isNested); - } - } - } - - /** - * Returns a human-readable reason for a function node type. - */ - private getFunctionReason(nodeType: string): string { - switch (nodeType) { - case "arrow_function": - return "arrow function (nested)"; - case "function_expression": - return "function expression (nested)"; - case "method_definition": - return "method (nested)"; - default: - return "function (nested)"; - } - } - - /** - * Extracts the name of a function from its AST node. - * - * @param node - The function AST node - * @returns The function name or a descriptive placeholder - */ - private getFunctionName(node: Parser.SyntaxNode): string { - if (node.type === "function_declaration" || node.type === "function_expression") { - const nameNode = node.children.find((c) => c.type === "identifier"); - if (nameNode) { - return this.sourceText.substring(nameNode.startIndex, nameNode.endIndex); - } - } - - if (node.type === "method_definition") { - const nameNode = node.children.find( - (c) => c.type === "property_identifier" || c.type === "identifier" - ); - if (nameNode) { - return this.sourceText.substring(nameNode.startIndex, nameNode.endIndex); - } - } - - if (node.type === "arrow_function") { - const parent = node.parent; - if (parent && parent.type === "variable_declarator") { - const nameNode = parent.children.find((c) => c.type === "identifier"); - if (nameNode) { - return this.sourceText.substring(nameNode.startIndex, nameNode.endIndex); - } - } - return "(arrow function)"; - } - - return "(anonymous)"; - } - - /** - * Analyzes a single AST node and its children for complexity contributions. - * - * @param node - The AST node to analyze - * @param skipSelfIncrement - When true, skip incrementing complexity for this node (used for else-if) - */ - private analyzeNode(node: Parser.SyntaxNode, skipSelfIncrement = false): void { - if (!skipSelfIncrement) { - const increment = this.getComplexityIncrement(node); - if (increment > 0) { - this.details.push({ - increment, - reason: this.getComplexityReason(node), - line: node.startPosition.row, - column: node.startPosition.column, - nesting: this.nesting, - }); - this.complexity += increment; - } - } - - const nestingIncreased = this.increasesNesting(node); - if (nestingIncreased) { - this.nesting++; - } - - for (const child of node.children) { - // Skip nested function bodies - they are analyzed separately - if (this.isNestedFunction(child)) { - this.collectFunctions(child, [], true); - continue; - } - // For else_clause containing if_statement (else-if): - // The else_clause is already counted above. The inner if_statement's structural - // increment is skipped to avoid double-counting, but its body is still analyzed. - if (node.type === "else_clause" && child.type === "if_statement") { - this.analyzeNode(child, true); - continue; - } - this.analyzeNode(child); - } - - if (nestingIncreased) { - this.nesting--; - } - } - - /** - * Checks if a node is a nested function definition. - */ - private isNestedFunction(node: Parser.SyntaxNode): boolean { - return ( - node.type === "function_expression" || - node.type === "arrow_function" || - node.type === "method_definition" - ); - } - - /** - * Calculates the complexity increment for a specific syntax node type. - * - * @param node - The syntax node to evaluate - * @returns The complexity increment (0 or positive integer) - */ - private getComplexityIncrement(node: Parser.SyntaxNode): number { - switch (node.type) { - // Control flow statements (+1 + nesting) - case "if_statement": - case "for_statement": - case "for_in_statement": - case "for_of_statement": - case "while_statement": - case "do_statement": - case "switch_statement": - return 1 + this.nesting; - - // Else/else-if clauses (+1, flat) - case "else_clause": - return 1; - - // Exception handling (+1) - case "catch_clause": - return 1; - - // Ternary expressions (+1) - case "ternary_expression": - return 1; - - // Logical operators (+1 each) - case "binary_expression": - case "logical_expression": { - const op = this.getOperator(node); - if (op === "&&" || op === "||" || op === "??") { - return 1; - } - return 0; - } - - // Labeled break/continue (+1) - case "break_statement": - case "continue_statement": { - const hasLabel = node.children.some( - (c) => c.type === "statement_identifier" - ); - return hasLabel ? 1 : 0; - } - - default: - return 0; - } - } - - /** - * Extracts the operator from a binary or logical expression node. - */ - private getOperator(node: Parser.SyntaxNode): string | null { - for (const child of node.children) { - const text = this.sourceText.substring(child.startIndex, child.endIndex); - if (text === "&&" || 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_statement": - return "if statement"; - case "else_clause": { - const hasNestedIf = node.children.some((c) => c.type === "if_statement"); - return hasNestedIf ? "else if clause" : "else clause"; - } - case "for_statement": - return "for loop"; - case "for_in_statement": - return "for...in loop"; - case "for_of_statement": - return "for...of loop"; - case "while_statement": - return "while loop"; - case "do_statement": - return "do...while loop"; - case "switch_statement": - return "switch statement"; - case "catch_clause": - return "catch clause"; - case "ternary_expression": - return "ternary expression"; - case "binary_expression": - case "logical_expression": { - const op = this.getOperator(node); - return `logical ${op} operator`; - } - case "break_statement": - return "labeled break statement"; - case "continue_statement": - return "labeled continue statement"; - default: - return "complexity source"; - } - } - - /** - * 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_statement" || - node.type === "for_statement" || - node.type === "for_in_statement" || - node.type === "for_of_statement" || - node.type === "while_statement" || - node.type === "do_statement" || - node.type === "switch_statement" || - node.type === "catch_clause" - ); + super(_parser); } /** @@ -425,17 +43,8 @@ export class TypeScriptMetricsAnalyzer { * * @param sourceText - The complete TypeScript source code to analyze * @returns An array of complexity analysis results for all functions found - * - * @example - * ```typescript - * const results = TypeScriptMetricsAnalyzer.analyzeFile(tsCode); - * results.forEach(func => { - * console.log(`${func.name}: ${func.complexity}`); - * }); - * ``` */ - public static analyzeFile(sourceText: string): TypeScriptFunctionMetrics[] { - const analyzer = new TypeScriptMetricsAnalyzer(); - return analyzer.analyzeFunctions(sourceText); + public static analyzeFile(sourceText: string): JsLikeFunctionMetrics[] { + return new TypeScriptMetricsAnalyzer().analyzeFunctions(sourceText); } } From d67070503c37ad57d713425dd95b7402e7a533de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Wed, 22 Apr 2026 17:18:18 +0100 Subject: [PATCH 2/3] test: add tests for JsLikeMetricsAnalyzer base class coverage gaps Add dedicated test suite for the shared base class exercising: - Nested method_definition and function_declaration reason strings - Anonymous function expression naming ('(anonymous)') - Unlabeled continue (no complexity) - for...in vs for...of detection - Deep nesting with mixed control flow - Catch clause nesting - do...while inside for loop - Nested ternary expressions - Logical operator combinations inside control flow - Nested function complexity isolation - Empty/edge cases (empty body, empty source, positions) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../languages/jsLikeAnalyzer.test.ts | 342 ++++++++++++++++++ src/test/suite/index.ts | 1 + 2 files changed, 343 insertions(+) create mode 100644 src/test/metricsAnalyzer/languages/jsLikeAnalyzer.test.ts diff --git a/src/test/metricsAnalyzer/languages/jsLikeAnalyzer.test.ts b/src/test/metricsAnalyzer/languages/jsLikeAnalyzer.test.ts new file mode 100644 index 0000000..e28892d --- /dev/null +++ b/src/test/metricsAnalyzer/languages/jsLikeAnalyzer.test.ts @@ -0,0 +1,342 @@ +import * as assert from "assert"; +import { JavaScriptMetricsAnalyzer } from "../../../metricsAnalyzer/languages/javascriptAnalyzer"; + +/** + * Tests for the shared JsLikeMetricsAnalyzer base class, exercised via JavaScriptMetricsAnalyzer. + * Focuses on code paths not covered by the language-specific test suites. + */ +suite("JsLikeMetricsAnalyzer Base Class Tests", () => { + suite("getFunctionReason - nested function types", () => { + test("should report 'method (nested)' for nested class method", () => { + const sourceCode = ` +function outer() { + class Inner { + method() { + return 1; + } + } +} +`; + const results = JavaScriptMetricsAnalyzer.analyzeFile(sourceCode); + const outerFunc = results.find((r) => r.name === "outer"); + assert.ok(outerFunc); + const methodReason = outerFunc!.details.find((d) => + d.reason === "method (nested)" + ); + assert.ok(methodReason, "Expected 'method (nested)' reason for nested class method"); + }); + + test("should report 'function (nested)' for nested function declaration", () => { + const sourceCode = ` +function outer() { + function inner() { + return 1; + } +} +`; + const results = JavaScriptMetricsAnalyzer.analyzeFile(sourceCode); + // inner is a function_declaration nested inside outer, should get "function (nested)" as default + // But function_declaration is not treated as isNestedFunction (only function_expression, arrow_function, method_definition) + // So inner is collected as a separate top-level function + assert.strictEqual(results.length, 2); + const outerFunc = results.find((r) => r.name === "outer"); + const innerFunc = results.find((r) => r.name === "inner"); + assert.ok(outerFunc); + assert.ok(innerFunc); + assert.strictEqual(innerFunc!.complexity, 0); + }); + }); + + suite("getFunctionName - edge cases", () => { + test("should return '(anonymous)' for anonymous function expression not assigned to variable", () => { + const sourceCode = ` +(function() { + if (true) { + return 1; + } +})(); +`; + const results = JavaScriptMetricsAnalyzer.analyzeFile(sourceCode); + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].name, "(anonymous)"); + }); + + test("should extract name from named function expression", () => { + const sourceCode = ` +const x = function myFunc() { + return 1; +}; +`; + const results = JavaScriptMetricsAnalyzer.analyzeFile(sourceCode); + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].name, "myFunc"); + }); + + test("should extract method name from class method", () => { + const sourceCode = ` +class Foo { + myMethod() { + if (true) { return 1; } + } +} +`; + const results = JavaScriptMetricsAnalyzer.analyzeFile(sourceCode); + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].name, "myMethod"); + }); + }); + + suite("Complexity increments - edge cases", () => { + test("should not count binary expression without logical operator", () => { + const sourceCode = ` +function add(a, b) { + return a + b; +} +`; + const results = JavaScriptMetricsAnalyzer.analyzeFile(sourceCode); + assert.strictEqual(results[0].complexity, 0); + }); + + test("should count for...in loop as for_in_statement", () => { + const sourceCode = ` +function iterate(obj) { + for (const key in obj) { + console.log(key); + } +} +`; + const results = JavaScriptMetricsAnalyzer.analyzeFile(sourceCode); + assert.strictEqual(results[0].complexity, 1); + assert.strictEqual(results[0].details[0].reason, "for...in loop"); + }); + + test("should count for...of loop as for_in_statement with of child", () => { + const sourceCode = ` +function iterate(arr) { + for (const item of arr) { + console.log(item); + } +} +`; + const results = JavaScriptMetricsAnalyzer.analyzeFile(sourceCode); + assert.strictEqual(results[0].complexity, 1); + assert.strictEqual(results[0].details[0].reason, "for...of loop"); + }); + + test("should not add complexity for unlabeled continue", () => { + const sourceCode = ` +function skip(arr) { + for (let i = 0; i < arr.length; i++) { + if (arr[i] < 0) { + continue; + } + console.log(arr[i]); + } +} +`; + const results = JavaScriptMetricsAnalyzer.analyzeFile(sourceCode); + const reasons = results[0].details.map((d) => d.reason); + assert.ok(!reasons.includes("labeled continue statement")); + }); + }); + + suite("Nesting and complexity details", () => { + test("should apply correct nesting for deeply nested control flow", () => { + const sourceCode = ` +function deep(a, b, c, d) { + if (a) { + for (let i = 0; i < b; i++) { + while (c) { + if (d) { + return true; + } + } + } + } + return false; +} +`; + const results = JavaScriptMetricsAnalyzer.analyzeFile(sourceCode); + + // if(1+0) + for(1+1) + while(1+2) + if(1+3) = 1+2+3+4 = 10 + assert.strictEqual(results[0].complexity, 10); + assert.strictEqual(results[0].details.length, 4); + assert.strictEqual(results[0].details[0].nesting, 0); // outer if + assert.strictEqual(results[0].details[1].nesting, 1); // for + assert.strictEqual(results[0].details[2].nesting, 2); // while + assert.strictEqual(results[0].details[3].nesting, 3); // inner if + }); + + test("should handle catch clause with nesting", () => { + const sourceCode = ` +function safeParse(text) { + if (text) { + try { + return JSON.parse(text); + } catch (e) { + if (e instanceof SyntaxError) { + return null; + } + } + } +} +`; + const results = JavaScriptMetricsAnalyzer.analyzeFile(sourceCode); + + // if(1+0) + catch(1) + if inside catch(1+2) = 1+1+3 = 5 + assert.strictEqual(results[0].complexity, 5); + const catchDetail = results[0].details.find((d) => d.reason === "catch clause"); + assert.ok(catchDetail); + assert.strictEqual(catchDetail!.increment, 1); + }); + + test("should handle switch nested inside if", () => { + const sourceCode = ` +function process(type, value) { + if (value) { + switch (type) { + case "a": return 1; + case "b": return 2; + default: return 0; + } + } +} +`; + const results = JavaScriptMetricsAnalyzer.analyzeFile(sourceCode); + + // if(1+0) + switch(1+1) = 1+2 = 3 + assert.strictEqual(results[0].complexity, 3); + }); + + test("should handle do...while nested inside for loop", () => { + const sourceCode = ` +function retry(attempts) { + for (let i = 0; i < attempts.length; i++) { + do { + attempts[i]--; + } while (attempts[i] > 0); + } +} +`; + const results = JavaScriptMetricsAnalyzer.analyzeFile(sourceCode); + + // for(1+0) + do(1+1) = 1+2 = 3 + assert.strictEqual(results[0].complexity, 3); + }); + }); + + suite("Nested function complexity isolation", () => { + test("should isolate nested arrow function complexity from outer", () => { + const sourceCode = ` +function outer() { + if (true) { + const fn = () => { + if (false) { + return 1; + } + return 0; + }; + return fn; + } +} +`; + const results = JavaScriptMetricsAnalyzer.analyzeFile(sourceCode); + + // Outer: if(1) + arrow nested at nesting=1 (1+1=2) = 3 + const outerFunc = results.find((r) => r.name === "outer"); + assert.ok(outerFunc); + assert.strictEqual(outerFunc!.complexity, 3); + }); + + test("should handle nested function expression with its own complexity", () => { + const sourceCode = ` +function container() { + const helper = function processor() { + if (true) { + for (let i = 0; i < 10; i++) { + console.log(i); + } + } + }; +} +`; + const results = JavaScriptMetricsAnalyzer.analyzeFile(sourceCode); + + // container: function_expression nested at nesting=0 → +1 + const container = results.find((r) => r.name === "container"); + assert.ok(container); + assert.strictEqual(container!.complexity, 1); + }); + }); + + suite("Logical operator combinations", () => { + test("should count mixed logical operators", () => { + const sourceCode = ` +function validate(a, b, c) { + return a && b || c ?? false; +} +`; + const results = JavaScriptMetricsAnalyzer.analyzeFile(sourceCode); + + // && + || + ?? = 3 + assert.strictEqual(results[0].complexity, 3); + }); + + test("should count logical operators inside control flow with nesting", () => { + const sourceCode = ` +function check(x, y) { + if (x > 0) { + if (x && y) { + return true; + } + } + return false; +} +`; + const results = JavaScriptMetricsAnalyzer.analyzeFile(sourceCode); + + // if(1+0) + if(1+1) + &&(1) = 1+2+1 = 4 + assert.strictEqual(results[0].complexity, 4); + }); + }); + + suite("Ternary expression details", () => { + test("should count nested ternary expressions", () => { + const sourceCode = ` +function classify(n) { + return n > 0 ? "positive" : n < 0 ? "negative" : "zero"; +} +`; + const results = JavaScriptMetricsAnalyzer.analyzeFile(sourceCode); + + // two ternary expressions, each +1 + assert.strictEqual(results[0].complexity, 2); + }); + }); + + suite("Empty and edge cases", () => { + test("should handle empty function body", () => { + const sourceCode = ` +function noop() {} +`; + const results = JavaScriptMetricsAnalyzer.analyzeFile(sourceCode); + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].complexity, 0); + assert.strictEqual(results[0].details.length, 0); + }); + + test("should handle empty source code", () => { + const results = JavaScriptMetricsAnalyzer.analyzeFile(""); + assert.strictEqual(results.length, 0); + }); + + test("should report correct end positions", () => { + const sourceCode = `function foo() { + if (true) {} +}`; + const results = JavaScriptMetricsAnalyzer.analyzeFile(sourceCode); + assert.ok(results[0].endLine >= results[0].startLine); + assert.ok(results[0].endColumn >= 0); + }); + }); +}); diff --git a/src/test/suite/index.ts b/src/test/suite/index.ts index ac247de..da26ac1 100644 --- a/src/test/suite/index.ts +++ b/src/test/suite/index.ts @@ -14,6 +14,7 @@ export function run(): Promise { "../metricsAnalyzer/languages/goAnalyzer.test", "../metricsAnalyzer/languages/javascriptAnalyzer.test", "../metricsAnalyzer/languages/typescriptAnalyzer.test", + "../metricsAnalyzer/languages/jsLikeAnalyzer.test", "../providers/codeLensProvider.test", ]; From a66540a1b35f061a1e7252a6c3337e13920c39a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Silva?= <2493377+askpt@users.noreply.github.com> Date: Wed, 22 Apr 2026 17:20:25 +0100 Subject: [PATCH 3/3] fix: correct assertion in nested function_declaration test function_declaration is not in isNestedFunction(), so it is analyzed as part of the outer function body rather than collected as a separate function. Fix the test to assert 1 result instead of 2. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../languages/jsLikeAnalyzer.test.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/test/metricsAnalyzer/languages/jsLikeAnalyzer.test.ts b/src/test/metricsAnalyzer/languages/jsLikeAnalyzer.test.ts index e28892d..72116f8 100644 --- a/src/test/metricsAnalyzer/languages/jsLikeAnalyzer.test.ts +++ b/src/test/metricsAnalyzer/languages/jsLikeAnalyzer.test.ts @@ -26,7 +26,7 @@ function outer() { assert.ok(methodReason, "Expected 'method (nested)' reason for nested class method"); }); - test("should report 'function (nested)' for nested function declaration", () => { + test("should treat nested function_declaration as part of outer function", () => { const sourceCode = ` function outer() { function inner() { @@ -35,15 +35,11 @@ function outer() { } `; const results = JavaScriptMetricsAnalyzer.analyzeFile(sourceCode); - // inner is a function_declaration nested inside outer, should get "function (nested)" as default - // But function_declaration is not treated as isNestedFunction (only function_expression, arrow_function, method_definition) - // So inner is collected as a separate top-level function - assert.strictEqual(results.length, 2); - const outerFunc = results.find((r) => r.name === "outer"); - const innerFunc = results.find((r) => r.name === "inner"); - assert.ok(outerFunc); - assert.ok(innerFunc); - assert.strictEqual(innerFunc!.complexity, 0); + // function_declaration is not in isNestedFunction, so inner is analyzed + // as part of outer's body rather than collected separately + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].name, "outer"); + assert.strictEqual(results[0].complexity, 0); }); });