@unito/integration-sdk
Version:
Integration SDK
116 lines (107 loc) • 3.64 kB
text/typescript
/**
* 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.<X>` 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;
}