/** * Shared AST helpers for `@unito/integration-sdk` ESLint rules. Centralised so * rules that walk the same shapes (catch clauses, logger member chains, * HttpErrors call sites) don't drift across rule files. */ import type * as ESTree from 'estree'; import type { Rule } from 'eslint'; export const HTTP_ERRORS_NAMESPACE = 'HttpErrors'; export const LOGGER_NAME_BROAD_RE = /\b(logger|console|log)\b/i; export const LOGGER_NAME_STRICT_RE = /\blogger\b/i; type NodeWithParent = ESTree.Node & { parent?: NodeWithParent }; export interface MemberChain { root: string | null; props: string[]; } /** * Walk a (possibly chained) MemberExpression and return its identifier * components. * * foo.bar.baz() → { root: 'foo', props: ['baz', 'bar'] } * this.x.y → { root: null, props: ['y', 'x'] } */ export function walkMemberChain(node: ESTree.Node | null | undefined): MemberChain { const props: string[] = []; let current: ESTree.Node | null | undefined = node; while (current && current.type === 'MemberExpression') { if (current.property && current.property.type === 'Identifier') { props.push(current.property.name); } current = current.object; } const root = current && current.type === 'Identifier' ? current.name : null; return { root, props }; } /** * Walks parents from `node` until either: * - finds a CatchClause (returns true) * - crosses a function boundary (returns false) * * Use to scope rules to lexically-enclosed catch blocks; helpers called from a * catch via a separate function are intentionally NOT matched. */ export function isInsideCatchClause(node: Rule.Node): boolean { let parent: NodeWithParent | undefined = (node as unknown as NodeWithParent).parent; while (parent) { if (parent.type === 'CatchClause') { return true; } if ( parent.type === 'FunctionDeclaration' || parent.type === 'FunctionExpression' || parent.type === 'ArrowFunctionExpression' ) { return false; } parent = parent.parent; } return false; } /** * Like isInsideCatchClause but returns the parameter name of the enclosing * CatchClause. Returns null if the node is not inside a catch, or if the catch * uses destructuring / has no parameter binding. */ export function getCatchClauseParam(node: Rule.Node): string | null { let parent: NodeWithParent | undefined = (node as unknown as NodeWithParent).parent; while (parent) { if (parent.type === 'CatchClause') { const catchClause = parent as unknown as ESTree.CatchClause; if (catchClause.param && catchClause.param.type === 'Identifier') { return catchClause.param.name; } return null; } if ( parent.type === 'FunctionDeclaration' || parent.type === 'FunctionExpression' || parent.type === 'ArrowFunctionExpression' ) { return null; } parent = parent.parent; } return null; } /** * Returns true if `callee` is a `HttpErrors.` MemberExpression. If `subClass` * is provided, additionally requires the property name to match. */ export function isHttpErrorsCallee(callee: ESTree.Node | null | undefined, subClass?: string): boolean { if (!callee || callee.type !== 'MemberExpression') { return false; } if (!callee.object || callee.object.type !== 'Identifier') { return false; } if (callee.object.name !== HTTP_ERRORS_NAMESPACE) { return false; } if (subClass === undefined) { return true; } if (!callee.property || callee.property.type !== 'Identifier') { return false; } return callee.property.name === subClass; }