pi-lens
Version:
Real-time code feedback for pi โ LSP, linters, formatters, type-checking, structural analysis & booboo
443 lines (441 loc) โข 14.2 kB
JavaScript
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
};