UNPKG

pi-lens

Version:

Real-time code feedback for pi โ€” LSP, linters, formatters, type-checking, structural analysis & booboo

443 lines (441 loc) โ€ข 14.2 kB
import { createRequire as __pilensCreateRequire } from "node:module"; const require = __pilensCreateRequire(import.meta.url); import { calcCyclomaticComplexity } from "./chunk-YLF3PPT5.js"; import "./chunk-JOLITGCC.js"; import "./chunk-5W3R6LFT.js"; import { firstChildOfType, isComplexityLanguageId, isComplexitySupportedFile, resolveTreeSitterLanguage, walk, withTreeSitterRoot } from "./chunk-LW6XKDZQ.js"; import "./chunk-Z54ZSOBZ.js"; import "./chunk-TGPXLY2J.js"; import "./chunk-QOZXJ4NA.js"; import "./chunk-NTUJEGDZ.js"; import "./chunk-WONERJTP.js"; import "./chunk-VN2AXLEX.js"; import "./chunk-5ZEJHV35.js"; import "./chunk-DQ2NZ7C4.js"; import "./chunk-VNMT7C64.js"; import "./chunk-2EECBNC5.js"; import "./chunk-N3YQJI6O.js"; import { createSubsystemLogger } from "./chunk-O6TQT6RI.js"; import "./chunk-5THXD6X5.js"; import "./chunk-UK3CMEAL.js"; import "./chunk-LJDODBBZ.js"; import "./chunk-ABBOA7UD.js"; import "./chunk-Q5U6FMDI.js"; import "./chunk-NXL4FFQQ.js"; // dist/clients/complexity-client.js import * as fs from "node:fs"; import * as path from "node:path"; var JSTS = { functionLike: /* @__PURE__ */ new Set([ "function_declaration", "method_definition", "function_expression", "arrow_function", "generator_function", "generator_function_declaration" ]), nesting: /* @__PURE__ */ new Set([ "if_statement", "while_statement", "for_statement", "for_in_statement", "switch_statement", "function_declaration", "function_expression", "arrow_function", "method_definition", "class_declaration", "try_statement", "catch_clause" ]), decision: /* @__PURE__ */ new Set([ "if_statement", "while_statement", "for_statement", "for_in_statement", "switch_case", "ternary_expression" ]), cognitive: /* @__PURE__ */ new Set([ "if_statement", "while_statement", "for_statement", "for_in_statement", "switch_statement", "switch_case", "ternary_expression", "catch_clause" ]), logicalOpNodes: /* @__PURE__ */ new Set(), logicalBinaryOps: /* @__PURE__ */ new Set(["&&", "||", "??"]), tryNode: "try_statement", nameChildTypes: ["identifier", "property_identifier"] }; var PYTHON = { functionLike: /* @__PURE__ */ new Set(["function_definition", "lambda"]), nesting: /* @__PURE__ */ new Set([ "if_statement", "for_statement", "while_statement", "match_statement", "function_definition", "class_definition", "try_statement", "except_clause", "elif_clause" ]), decision: /* @__PURE__ */ new Set([ "if_statement", "elif_clause", "for_statement", "while_statement", "case_clause", "conditional_expression" ]), cognitive: /* @__PURE__ */ new Set([ "if_statement", "elif_clause", "for_statement", "while_statement", "match_statement", "case_clause", "conditional_expression", "except_clause" ]), logicalOpNodes: /* @__PURE__ */ new Set(["boolean_operator"]), logicalBinaryOps: /* @__PURE__ */ new Set(), tryNode: "try_statement", nameChildTypes: ["identifier"] }; var GO = { functionLike: /* @__PURE__ */ new Set([ "function_declaration", "method_declaration", "func_literal" ]), nesting: /* @__PURE__ */ new Set([ "if_statement", "for_statement", "expression_switch_statement", "type_switch_statement", "select_statement", "function_declaration", "method_declaration" ]), decision: /* @__PURE__ */ new Set([ "if_statement", "for_statement", "expression_case", "type_case", "communication_case" ]), cognitive: /* @__PURE__ */ new Set([ "if_statement", "for_statement", "expression_switch_statement", "type_switch_statement", "select_statement", "expression_case", "type_case", "communication_case" ]), logicalOpNodes: /* @__PURE__ */ new Set(), logicalBinaryOps: /* @__PURE__ */ new Set(["&&", "||"]), tryNode: void 0, // Go has no try/catch (error returns) nameChildTypes: ["identifier", "field_identifier"] }; var RUST = { functionLike: /* @__PURE__ */ new Set(["function_item", "closure_expression"]), nesting: /* @__PURE__ */ new Set([ "if_expression", "while_expression", "for_expression", "loop_expression", "match_expression", "function_item" ]), decision: /* @__PURE__ */ new Set([ "if_expression", "while_expression", "for_expression", "loop_expression", "match_arm" ]), cognitive: /* @__PURE__ */ new Set([ "if_expression", "while_expression", "for_expression", "loop_expression", "match_expression", "match_arm" ]), logicalOpNodes: /* @__PURE__ */ new Set(), logicalBinaryOps: /* @__PURE__ */ new Set(["&&", "||"]), tryNode: void 0, // Rust uses Result/? โ€” no try/catch nameChildTypes: ["identifier"] }; var LANGUAGE_NODES = { typescript: JSTS, tsx: JSTS, javascript: JSTS, python: PYTHON, go: GO, rust: RUST }; var COMMENT_TYPES = /* @__PURE__ */ new Set(["comment", "line_comment", "block_comment"]); function isLogicalOp(node, nodes) { if (nodes.logicalOpNodes.has(node.type)) return true; if (nodes.logicalBinaryOps.size > 0 && node.type === "binary_expression") { return (node.children ?? []).some((c) => c && nodes.logicalBinaryOps.has(c.type)); } return false; } function subtreeCyclomatic(root, nodes) { if (nodes === JSTS) return calcCyclomaticComplexity(root) - 1; let cc = 0; walk(root, (n) => { if (nodes.decision.has(n.type)) cc++; if (isLogicalOp(n, nodes)) cc++; }); return cc; } function subtreeCognitive(node, nesting, nodes) { let complexity = 0; if (nodes.cognitive.has(node.type)) complexity += 1 + nesting; if ((node.type === "break_statement" || node.type === "continue_statement") && (node.children ?? []).some((c) => c && (c.type === "statement_identifier" || c.type === "identifier" || c.type === "label_name" || c.type === "label"))) { complexity += 1 + nesting; } if (isLogicalOp(node, nodes)) complexity += 1; const childNesting = nodes.nesting.has(node.type) ? nesting + 1 : nesting; for (const child of node.children ?? []) { if (child) complexity += subtreeCognitive(child, childNesting, nodes); } return complexity; } function subtreeMaxNesting(node, currentDepth, nodes) { let maxDepth = currentDepth; if (nodes.nesting.has(node.type)) { currentDepth++; maxDepth = Math.max(maxDepth, currentDepth); } for (const child of node.children ?? []) { if (child) { maxDepth = Math.max(maxDepth, subtreeMaxNesting(child, currentDepth, nodes)); } } return maxDepth; } function functionName(fnNode, nodes) { for (const t of nodes.nameChildTypes) { const id = firstChildOfType(fnNode, t); if (id) return id.text; } return void 0; } function collectFunctionMetrics(root, nodes) { const functions = []; const visit = (node, nestingLevel) => { if (nodes.functionLike.has(node.type)) { const startLine = node.startPosition.row; const endLine = node.endPosition.row; functions.push({ name: functionName(node, nodes) ?? `<anonymous@L${startLine + 1}>`, line: startLine + 1, length: endLine - startLine + 1, cyclomatic: subtreeCyclomatic(node, nodes), cognitive: subtreeCognitive(node, nestingLevel, nodes), nestingDepth: subtreeMaxNesting(node, 0, nodes) }); } const newNesting = nodes.nesting.has(node.type) ? nestingLevel + 1 : nestingLevel; for (const child of node.children ?? []) { if (child) visit(child, newNesting); } }; visit(root, 0); return functions; } function countTryCatch(root, nodes) { if (!nodes.tryNode) return 0; let count = 0; walk(root, (n) => { if (n.type === nodes.tryNode) count++; }); return count; } function countLines(content, root) { const lines = content.split(/\r?\n/); const commentLineSet = /* @__PURE__ */ new Set(); walk(root, (n) => { if (COMMENT_TYPES.has(n.type)) { for (let l = n.startPosition.row; l <= n.endPosition.row; l++) { commentLineSet.add(l); } } }); const codeLines = lines.filter((line, i) => { if (line.trim().length === 0) return false; if (!commentLineSet.has(i)) return true; const stripped = line.replace(/\/\/.*$/, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/#.*$/, "").trim(); return stripped.length > 0; }).length; return { codeLines, commentLines: commentLineSet.size }; } var AI_COMMENT_PATTERNS = [ /(?:๐Ÿ”|โœ…|๐Ÿ“|๐Ÿ”ง|๐Ÿ›|โš ๏ธ|๐Ÿš€|๐Ÿ’ก|๐ŸŽฏ|๐Ÿ“Œ|๐Ÿท๏ธ|๐Ÿ”‘|๐Ÿ—๏ธ|๐Ÿงช|๐Ÿ—‘๏ธ|๐Ÿ”„|โ™ป๏ธ|๐Ÿ“‹|๐Ÿ”–|๐Ÿ“Š|๐Ÿ’ฌ|๐Ÿ”ฅ|๐Ÿ’Ž|โญ|๐ŸŒŸ|๐ŸŽจ|๐Ÿ› ๏ธ)/u, /(?:\/\/|#)\s*(Initialize|Setup|Clean up|Create|Define|Check if|Handle|Process|Validate|Return|Get|Set|Add|Remove|Update|Fetch)\b/i, /(?:\/\/|#)\s*(This function|This method|This code|Here we|Now we)\b/i, /\/\*\*?\s*(Overview|Summary|Description|Example|Usage)\s*\*?\//i ]; function countAICommentPatterns(sourceText) { let count = 0; for (const line of sourceText.split(/\r?\n/)) { const trimmed = line.trim(); if (trimmed.startsWith("//") || trimmed.startsWith("/*") || trimmed.startsWith("*") || trimmed.startsWith("#")) { for (const pattern of AI_COMMENT_PATTERNS) { if (pattern.test(line)) { count++; break; } } } } return count; } function calculateCodeEntropy(sourceText) { const tokens = sourceText.replace(/\/\/.*/g, "").replace(/\/\*[\s\S]*?\*\//g, "").replace(/["'`][^"'`]*["'`]/g, "STR").replace(/\b\d+(\.\d+)?\b/g, "NUM").split(/[\s\n\r\t,;:()[\]{}=<>!&|+\-*/%^~?]+/).filter((t) => t.length > 0); if (tokens.length === 0) return 0; const freq = /* @__PURE__ */ new Map(); for (const token of tokens) freq.set(token, (freq.get(token) || 0) + 1); let entropy = 0; for (const count of freq.values()) { const p = count / tokens.length; if (p > 0) entropy -= p * Math.log2(p); } return entropy; } function calculateMaintainabilityIndex(cyclomatic, loc, comments) { if (loc === 0) return 100; const lnLOC = Math.log(loc); let mi = (171 - 0.23 * cyclomatic - 16.2 * lnLOC) * 100 / 171; const commentBonus = Math.min(10, comments / loc * 50); mi += commentBonus; return Math.max(0, Math.min(100, mi)); } function calculateMaxParams(functions) { return Math.min(10, Math.max(2, Math.round(functions.reduce((a, f) => a + f.length, 0) / Math.max(1, functions.length) / 5))); } function countSingleUseFunctions(functions) { return functions.filter((f) => f.length < 10 && f.cyclomatic <= 2 && /^(get|set|check|is|has|validate|format|parse|convert|create|make)/i.test(f.name)).length; } var ComplexityClient = class { log; constructor(verbose = false) { this.log = verbose ? createSubsystemLogger("complexity") : () => { }; } /** True if the file's grammar has a complexity node mapping. * * Delegates to the module-free predicate in `tree-sitter-shared.ts`: the * same question is asked by `handleToolCallImpl` BEFORE the analyzer * bootstrap is loaded (#2467), and two copies of it would be two answers. */ isSupportedFile(filePath) { return isComplexitySupportedFile(filePath); } /** Analyze complexity metrics for a file (null if unsupported / unparseable). */ async analyzeFile(filePath) { const absolutePath = path.resolve(filePath); const languageId = resolveTreeSitterLanguage(absolutePath); const nodes = languageId && isComplexityLanguageId(languageId) ? LANGUAGE_NODES[languageId] : void 0; if (!nodes) return null; let content; try { if (!fs.existsSync(absolutePath)) return null; content = fs.readFileSync(absolutePath, "utf-8"); } catch (err) { this.log(`Read error for ${filePath}: ${err.message}`); return null; } try { const parsed = await withTreeSitterRoot(absolutePath, content, (root) => this.computeMetrics(absolutePath, content, root, nodes), "complexity"); return parsed.parsed ? parsed.value : null; } catch (err) { this.log(`Analysis error for ${filePath}: ${err.message}`); return null; } } computeMetrics(absolutePath, content, root, nodes) { const { codeLines, commentLines } = countLines(content, root); const functions = collectFunctionMetrics(root, nodes); const maxNestingDepth = subtreeMaxNesting(root, 0, nodes); const cognitive = subtreeCognitive(root, 0, nodes); const funcStats = this.aggregateFunctionStats(functions); return { filePath: path.relative(process.cwd(), absolutePath), maxNestingDepth, avgFunctionLength: funcStats.avgLength, maxFunctionLength: funcStats.maxLength, functionCount: functions.length, cyclomaticComplexity: funcStats.avgCyclomatic, maxCyclomaticComplexity: funcStats.maxCyclomatic, cognitiveComplexity: cognitive, maintainabilityIndex: Math.round(calculateMaintainabilityIndex(funcStats.avgCyclomatic, codeLines, commentLines) * 10) / 10, linesOfCode: codeLines, commentLines, codeEntropy: Math.round(calculateCodeEntropy(content) * 100) / 100, maxParamsInFunction: calculateMaxParams(functions), aiCommentPatterns: countAICommentPatterns(content), singleUseFunctions: countSingleUseFunctions(functions), tryCatchCount: countTryCatch(root, nodes), functions }; } aggregateFunctionStats(functions) { if (functions.length === 0) { return { avgLength: 0, maxLength: 0, avgCyclomatic: 1, maxCyclomatic: 1 }; } const lengths = functions.map((f) => f.length); const cyclomatics = functions.map((f) => f.cyclomatic); const sum = (arr) => arr.reduce((a, b) => a + b, 0); return { avgLength: Math.round(sum(lengths) / lengths.length), maxLength: Math.max(...lengths), avgCyclomatic: Math.max(1, Math.round(sum(cyclomatics) / cyclomatics.length)), maxCyclomatic: Math.max(1, Math.max(...cyclomatics)) }; } }; export { ComplexityClient };