UNPKG

pi-lens

Version:

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

309 lines (306 loc) • 10.2 kB
import { createRequire as __pilensCreateRequire } from "node:module"; const require = __pilensCreateRequire(import.meta.url); import { extractFactsFromTree } from "./chunk-JOLITGCC.js"; import { findOwnerName } from "./chunk-5W3R6LFT.js"; import { firstChildOfType, walk } from "./chunk-LW6XKDZQ.js"; import { isJstsFactFile } from "./chunk-5ZEJHV35.js"; // dist/clients/dispatch/facts/function-facts.js var BOUNDARY_PREFIXES = [ "fetch", "fs.", "db.", "http", "axios", "got", "req.", "res." ]; var FUNCTION_TYPES = /* @__PURE__ */ new Set([ "function_declaration", "method_definition", "function_expression", "arrow_function" ]); var COMPLEXITY_TYPES = /* @__PURE__ */ new Set([ "if_statement", "for_statement", "for_in_statement", // for…of and for…in both parse to for_in_statement "while_statement", "do_statement", "switch_case", // `case` only — `default` (switch_default) is not counted "catch_clause", "ternary_expression" ]); var NESTING_TYPES = /* @__PURE__ */ new Set([ "if_statement", "for_statement", "for_in_statement", "while_statement", "do_statement", "switch_statement", "try_statement" ]); var LOGICAL_OPERATORS = /* @__PURE__ */ new Set(["&&", "||", "??"]); function namedChildren(node) { return (node.children ?? []).filter((c) => c && c.isNamed && c.type !== "comment"); } function firstNamedChild(node) { return namedChildren(node)[0]; } function getBody(node) { return firstChildOfType(node, "statement_block"); } function getParameters(node) { const fp = firstChildOfType(node, "formal_parameters"); if (!fp) return []; return (fp.children ?? []).filter((c) => c && (c.type === "identifier" || c.type === "required_parameter" || c.type === "optional_parameter" || c.type === "rest_pattern" || c.type === "assignment_pattern" || // Plain JavaScript's grammar places a top-level destructured // parameter directly as an object_pattern/array_pattern child // of formal_parameters — no required_parameter wrapper (the TS // grammar always wraps every parameter, destructured or not, // which is why the wrapper types above already covered TS). // Without this, `function f({a, b})` counted 0 params in JS // while the TS-annotated equivalent counted correctly (#1089 P3-4). c.type === "object_pattern" || c.type === "array_pattern")); } function parameterName(param) { return param.type === "identifier" ? param.text : firstNamedChild(param)?.text ?? ""; } function getFunctionName(node) { if (node.type === "function_declaration") { return firstChildOfType(node, "identifier")?.text ?? "<anonymous>"; } if (node.type === "method_definition") { const kids = node.children ?? []; const paramsIdx = kids.findIndex((c) => c?.type === "formal_parameters"); for (let i = paramsIdx - 1; i >= 0; i -= 1) { const t = kids[i]?.type; if (t === "property_identifier" || t === "private_property_identifier" || t === "computed_property_name" || t === "string" || t === "number") { return kids[i].text; } } return "<anonymous>"; } if (node.type === "arrow_function" || node.type === "function_expression") { const parent = node.parent; if (parent?.type === "variable_declarator") { const id = firstChildOfType(parent, "identifier"); if (id) return id.text; } else if (parent?.type === "pair") { const key = (parent.children ?? [])[0]; if (key) return key.text; } return "<anonymous>"; } return "<unknown>"; } function isCallPassThrough(stmt, paramNames) { if (stmt.type !== "return_statement") return { pass: false }; const expr = firstNamedChild(stmt); if (!expr || expr.type !== "call_expression") return { pass: false }; const argsNode = firstChildOfType(expr, "arguments"); const args = argsNode ? namedChildren(argsNode).map((a) => a.text) : []; if (args.length !== paramNames.length) return { pass: false }; for (let i = 0; i < args.length; i += 1) { if (args[i] !== paramNames[i]) return { pass: false }; } return { pass: true, target: (expr.children ?? [])[0]?.text }; } function calcCyclomaticComplexity(body) { let cc = 1; walk(body, (node) => { if (COMPLEXITY_TYPES.has(node.type)) { cc++; } else if (node.type === "binary_expression") { if ((node.children ?? []).some((c) => LOGICAL_OPERATORS.has(c?.type))) { cc++; } } }); return cc; } function calcMaxNestingDepth(body) { let maxDepth = 0; const walkDepth = (node, depth) => { if (depth > maxDepth) maxDepth = depth; const next = NESTING_TYPES.has(node.type) ? depth + 1 : depth; for (const child of node.children ?? []) { if (child) walkDepth(child, next); } }; for (const child of body.children ?? []) { if (child) walkDepth(child, 0); } return maxDepth; } function collectOutgoingCalls(body) { const calls = /* @__PURE__ */ new Set(); walk(body, (node) => { if (node.type === "call_expression") { const callee = (node.children ?? [])[0]?.text ?? ""; if (callee.length < 80) calls.add(callee); } }); return [...calls]; } function collectMemberCallSites(body) { const sites = []; walk(body, (node) => { if (node.type !== "call_expression") return; const callee = (node.children ?? [])[0]; if (!callee || callee.type !== "member_expression") return; const object = (callee.children ?? [])[0]; const property = (callee.children ?? []).find((c) => c?.type === "property_identifier"); if (!object || object.type !== "identifier" || !property) return; sites.push({ receiver: object.text, method: property.text }); }); return sites; } function collectReceiverTypes(body, params) { const types = {}; for (const param of params) { const id = firstNamedChild(param); if (!id || id.type !== "identifier") continue; const typeAnnotation = (param.children ?? []).find((c) => c?.type === "type_annotation"); const typeId = typeAnnotation ? firstChildOfType(typeAnnotation, "type_identifier") : void 0; if (typeId) types[id.text] = typeId.text; } walk(body, (node) => { if (node.type !== "variable_declarator") return; const id = firstChildOfType(node, "identifier"); if (!id) return; const value = (node.children ?? []).find((c) => c?.type === "new_expression"); if (!value) return; const ctor = firstNamedChild(value); if (ctor?.type === "identifier") types[id.text] = ctor.text; }); return types; } function hasExplicitPromiseReturnType(node) { const returnType = firstChildOfType(node, "type_annotation"); if (!returnType) return false; return /:\s*Promise\b/.test(returnType.text); } function hasAwaitInNode(node) { let found = false; walk(node, (n) => { if (n.type === "await_expression") found = true; }); return found; } function hasReturnAwaitCall(node) { let found = false; walk(node, (n) => { if (n.type !== "return_statement") return; const ret = firstNamedChild(n); if (ret?.type !== "await_expression") return; const awaited = firstNamedChild(ret); if (awaited?.type === "call_expression") found = true; }); return found; } var functionFactProvider = { id: "fact.file.functions", provides: ["file.functionSummaries", "file.functionFactsCoverage"], requires: ["file.content"], appliesTo(ctx) { return isJstsFactFile(ctx.filePath); }, async run(ctx, store) { await extractFactsFromTree(ctx, store, functionFactProvider.id, { "file.functionSummaries": [] }, (root) => { const summaries = []; const containers = []; const addSummary = (node) => { const body = getBody(node); if (!body) return; const params = getParameters(node); const paramNames = params.map(parameterName); const statements = namedChildren(body); const statementCount = statements.length; const passThrough = statementCount === 1 ? isCallPassThrough(statements[0], paramNames) : { pass: false }; const target = passThrough.target ?? ""; const lowerTarget = target.toLowerCase(); const isBoundaryWrapper = BOUNDARY_PREFIXES.some((prefix) => lowerTarget.startsWith(prefix)); summaries.push({ name: getFunctionName(node), line: node.startPosition.row + 1, column: node.startPosition.column + 1, endLine: body.endPosition.row + 1, isAsync: Boolean(firstChildOfType(node, "async")), hasAwait: hasAwaitInNode(body), hasReturnAwaitCall: hasReturnAwaitCall(body), statementCount, parameterCount: params.length, isPassThroughWrapper: passThrough.pass, passThroughTarget: passThrough.target, isBoundaryWrapper, hasExplicitPromiseReturnType: hasExplicitPromiseReturnType(node), cyclomaticComplexity: calcCyclomaticComplexity(body), maxNestingDepth: calcMaxNestingDepth(body), outgoingCalls: collectOutgoingCalls(body), memberCallSites: collectMemberCallSites(body), receiverTypes: collectReceiverTypes(body, params) }); }; walk(root, (node) => { if (FUNCTION_TYPES.has(node.type)) addSummary(node); if (node.type === "class_declaration" || node.type === "interface_declaration") { const nameNode = firstChildOfType(node, "type_identifier") ?? firstChildOfType(node, "identifier"); if (nameNode) { containers.push({ name: nameNode.text, startLine: node.startPosition.row + 1, endLine: node.endPosition.row + 1 }); } } }); for (const summary of summaries) { const owner = findOwnerName(containers, summary.line, summary.endLine ?? summary.line); if (owner) summary.owner = owner; } return { "file.functionSummaries": summaries }; }, "file.functionFactsCoverage"); } }; export { calcCyclomaticComplexity, functionFactProvider };