blob: e28e57761fdc8eb77f1cec24ab86960977312e06 [file] [log] [blame]
// Copyright 2025 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview A library for traversing the AST.
*/
import type {TSESTree} from '@typescript-eslint/utils';
import type {RuleContext, RuleListener} from '@typescript-eslint/utils/ts-eslint';
import type {DomFragment} from './dom-fragment';
type Node = TSESTree.Node;
type CallExpression = TSESTree.CallExpression;
type Identifier = TSESTree.Identifier;
type Context = RuleContext<'preferTemplateLiterals', []>;
type Subrule = Partial<{
getEvent(event: Node): string | null,
propertyAssignment(property: Identifier, propertyValue: Node, domFragment: DomFragment): boolean,
methodCall(property: Identifier, firstArg: Node, secondArg: Node, domFragment: DomFragment, call: CallExpression):
boolean,
propertyMethodCall(property: Identifier, method: Node, firstArg: Node, domFragment: DomFragment): boolean,
subpropertyAssignment(
property: Identifier, subproperty: Identifier, subpropertyValue: Node, domFragment: DomFragment): boolean,
functionCall(call: CallExpression, firstArg: Node, secondArg: Node, domFragment: DomFragment): boolean,
}>&Pick<RuleListener, 'MemberExpression'|'NewExpression'|'CallExpression'>;
export interface RuleCreator {
create(context: Context): Subrule;
}
export function isIdentifier(node: Node, name: string|string[]): boolean {
return node.type === 'Identifier' && (Array.isArray(name) ? name.includes(node.name) : node.name === name);
}
export function isIdentifierChain(node: Node, names: string[]): boolean {
if (names.length === 1) {
return isIdentifier(node, names[0]);
}
return Boolean(isMemberExpression(
node, n => isIdentifierChain(n, names.slice(0, -1)), n => isIdentifier(n, names.at(-1) as string)));
}
export function isMemberExpression(
node: Node, objectPredicate: (node: Node) => boolean, propertyPredicate: (node: Node) => boolean): Node|null {
const match = node.type === 'MemberExpression' && objectPredicate(node.object) && propertyPredicate(node.property);
return match ? node.property : null;
}
export function isLiteral(node: Node, value: string|number|boolean): boolean {
return node.type === 'Literal' && node.value === value;
}
export function getEnclosingExpression(node: Node): Node|null {
while (node.parent) {
if (node.parent.type === 'BlockStatement') {
return node;
}
node = node.parent;
}
return null;
}
export function getEnclosingProperty(node: Node): Node|null {
if (isMemberExpression(
node, n => n.type === 'ThisExpression', n => ['Identifier', 'PrivateIdentifier'].includes(n.type))) {
return node;
}
if (node.parent?.type === 'AssignmentExpression' && node.parent.right === node &&
isMemberExpression(
node.parent.left, n => n.type === 'ThisExpression',
n => ['Identifier', 'PrivateIdentifier'].includes(n.type))) {
return node.parent.left;
}
return null;
}
export function getEnclosingClassDeclaration(node: Node): Node|undefined {
let parent = node.parent;
while (parent && !['ClassDeclaration', 'ClassExpression'].includes(parent.type)) {
parent = parent.parent;
}
return parent;
}