AST utility module for statically analyzing JSX
npx @tessl/cli install tessl/npm-jsx-ast-utils@3.3.0jsx-ast-utils is an AST utility module for statically analyzing JSX syntax. Originally extracted from eslint-plugin-jsx-a11y, it provides essential functions for examining JSX elements and their properties, making it ideal for creating linting rules and static analysis tools for JSX code.
npm install jsx-ast-utilsimport { hasProp, getProp, getPropValue, elementType, eventHandlers } from "jsx-ast-utils";For CommonJS:
const { hasProp, getProp, getPropValue, elementType, eventHandlers } = require("jsx-ast-utils");Individual imports:
import hasProp from "jsx-ast-utils/hasProp";
import getProp from "jsx-ast-utils/getProp";
// Or equivalently:
const hasProp = require("jsx-ast-utils/hasProp");import { hasProp, getProp, elementType } from "jsx-ast-utils";
// In an ESLint rule or AST traversal
module.exports = context => ({
JSXOpeningElement: node => {
// Check if element has specific prop
const hasOnChange = hasProp(node.attributes, 'onChange');
// Get element tag name
const tagName = elementType(node);
// Get specific prop for further analysis
const onChangeProp = getProp(node.attributes, 'onChange');
if (hasOnChange && tagName === 'input') {
// Analyze the onChange prop...
}
}
});Functions for checking whether specific props exist on JSX elements.
/**
* Returns boolean indicating whether a prop exists on JSX element attributes
* @param props - Array of JSXAttribute nodes (usually node.attributes), defaults to empty array
* @param prop - String name of the prop to check for, defaults to empty string
* @param options - Configuration options, defaults to { ignoreCase: true, spreadStrict: true }
* @returns boolean indicating prop existence
*/
function hasProp(props: JSXAttribute[], prop: string, options: HasPropOptions): boolean;
/**
* Returns boolean indicating if any of the specified props exist on the node
* @param nodeProps - Array of JSXAttribute nodes, defaults to empty array
* @param props - Array of prop names or space-separated string, defaults to empty array
* @param options - Configuration options, defaults to { ignoreCase: true, spreadStrict: true }
* @returns boolean indicating if any prop exists
*/
function hasAnyProp(nodeProps: JSXAttribute[], props: string[] | string, options: HasPropOptions): boolean;
/**
* Returns boolean indicating if all of the specified props exist on the node
* @param nodeProps - Array of JSXAttribute nodes, defaults to empty array
* @param props - Array of prop names or space-separated string, defaults to empty array
* @param options - Configuration options, defaults to { ignoreCase: true, spreadStrict: true }
* @returns boolean indicating if all props exist
*/
function hasEveryProp(nodeProps: JSXAttribute[], props: string[] | string, options: HasPropOptions): boolean;
interface HasPropOptions {
/** Case insensitive matching (default: true) */
ignoreCase?: boolean;
/** Strict spread handling - assumes prop not in spread (default: true) */
spreadStrict?: boolean;
}Functions for retrieving JSX attributes and their associated data.
/**
* Returns the JSXAttribute itself or undefined if prop is not present
* @param props - Array of JSXAttribute nodes, defaults to empty array
* @param prop - String name of the prop to retrieve, defaults to empty string
* @param options - Configuration options, defaults to { ignoreCase: true }
* @returns JSXAttribute node or undefined
*/
function getProp(props: JSXAttribute[], prop: string, options: GetPropOptions): JSXAttribute | undefined;
interface GetPropOptions {
/** Case insensitive matching (default: true) */
ignoreCase?: boolean;
}Functions for extracting values from JSX attributes, handling various AST node types.
/**
* Returns the value of a JSXAttribute, extracting from various AST node types
* This function returns the most closely associated value with JSX intention
* @param attribute - JSXAttribute node from AST parser
* @returns Extracted value (any type depending on attribute content)
*/
function getPropValue(attribute: JSXAttribute): any;
/**
* Returns only literal values from JSXAttribute (strings, numbers, booleans, etc.)
* Returns undefined for complex expressions that cannot be statically analyzed
* @param attribute - JSXAttribute node from AST parser
* @returns Literal value or undefined
*/
function getLiteralPropValue(attribute: JSXAttribute): string | number | boolean | null | undefined;Functions for analyzing JSX element structure and names.
/**
* Returns the tagName associated with a JSXElement
* Handles member expressions (Foo.Bar), namespaced names (ns:name), and fragments
* @param node - JSXElement, JSXOpeningElement, or JSXOpeningFragment node, defaults to empty object
* @returns String representation of the element type (returns '<>' for fragments)
* @throws Error if node is not a valid JSX element
*/
function elementType(node: JSXElement | JSXOpeningElement | JSXOpeningFragment): string;
/**
* Returns the name of a JSXAttribute, handling namespaced attributes
* @param prop - JSXAttribute node from AST parser, defaults to empty object
* @returns String name of the attribute
* @throws Error if prop is not a JSXAttribute node
*/
function propName(prop: JSXAttribute): string;Pre-built collections of common JSX event handler names for validation and analysis.
/**
* Flat array of all common JSX event handler prop names
* Includes all DOM events: click, change, submit, keyboard, mouse, touch, etc.
*/
const eventHandlers: string[];
/**
* Event handlers organized by event type for targeted analysis
*/
const eventHandlersByType: {
clipboard: string[]; // onCopy, onCut, onPaste
composition: string[]; // onCompositionEnd, onCompositionStart, onCompositionUpdate
keyboard: string[]; // onKeyDown, onKeyPress, onKeyUp
focus: string[]; // onFocus, onBlur
form: string[]; // onChange, onInput, onSubmit
mouse: string[]; // onClick, onContextMenu, onDblClick, onDoubleClick, onDrag, etc.
selection: string[]; // onSelect
touch: string[]; // onTouchCancel, onTouchEnd, onTouchMove, onTouchStart
ui: string[]; // onScroll
wheel: string[]; // onWheel
media: string[]; // onAbort, onCanPlay, onCanPlayThrough, onDurationChange, etc.
image: string[]; // onLoad, onError
animation: string[]; // onAnimationStart, onAnimationEnd, onAnimationIteration
transition: string[]; // onTransitionEnd
};interface JSXAttribute {
type: 'JSXAttribute';
name: JSXIdentifier | JSXNamespacedName;
value?: JSXExpressionContainer | Literal | JSXElement | JSXFragment | null;
}
interface JSXIdentifier {
type: 'JSXIdentifier';
name: string;
}
interface JSXNamespacedName {
type: 'JSXNamespacedName';
namespace: JSXIdentifier;
name: JSXIdentifier;
}
interface JSXExpressionContainer {
type: 'JSXExpressionContainer';
expression: Expression;
}
interface JSXElement {
type: 'JSXElement';
openingElement: JSXOpeningElement;
closingElement?: JSXClosingElement;
children: (JSXText | JSXElement | JSXFragment | JSXExpressionContainer)[];
}
interface JSXFragment {
type: 'JSXFragment';
openingFragment: JSXOpeningFragment;
closingFragment: JSXClosingFragment;
children: (JSXText | JSXElement | JSXFragment | JSXExpressionContainer)[];
}
interface JSXClosingFragment {
type: 'JSXClosingFragment';
}
interface JSXText {
type: 'JSXText';
value: string;
raw: string;
}
interface JSXOpeningElement {
type: 'JSXOpeningElement';
name: JSXIdentifier | JSXMemberExpression | JSXNamespacedName;
attributes: (JSXAttribute | JSXSpreadAttribute)[];
selfClosing: boolean;
}
interface JSXOpeningFragment {
type: 'JSXOpeningFragment';
}
interface JSXClosingElement {
type: 'JSXClosingElement';
name: JSXIdentifier | JSXMemberExpression | JSXNamespacedName;
}
interface JSXMemberExpression {
type: 'JSXMemberExpression';
object: JSXIdentifier | JSXMemberExpression;
property: JSXIdentifier;
}
interface JSXSpreadAttribute {
type: 'JSXSpreadAttribute';
argument: Expression;
}
interface Literal {
type: 'Literal';
value: string | number | boolean | null | RegExp;
raw: string;
}
// Expression is a union of all possible JavaScript expression AST nodes
type Expression = any;import { hasProp, getPropValue, elementType, eventHandlers } from "jsx-ast-utils";
// Rule: Require alt prop on img elements
const requireAltRule = {
create(context) {
return {
JSXOpeningElement(node) {
if (elementType(node) === 'img') {
if (!hasProp(node.attributes, 'alt')) {
context.report({
node,
message: 'img elements must have an alt prop.'
});
}
}
}
};
}
};
// Rule: Check for event handler prop values
const checkEventHandlers = {
create(context) {
return {
JSXOpeningElement(node) {
node.attributes.forEach(attr => {
const propName = propName(attr);
if (eventHandlers.includes(propName)) {
const value = getPropValue(attr);
if (typeof value === 'string') {
context.report({
node: attr,
message: `Event handler ${propName} should not be a string.`
});
}
}
});
}
};
}
};import { elementType, hasProp, getProp, getLiteralPropValue } from "jsx-ast-utils";
function analyzeJSXElement(node) {
const tagName = elementType(node);
const hasClassName = hasProp(node.attributes, 'className');
const classNameProp = getProp(node.attributes, 'className');
const classNameValue = getLiteralPropValue(classNameProp);
return {
tagName,
hasClassName,
classNameValue,
isInteractive: node.attributes.some(attr =>
eventHandlers.includes(propName(attr))
)
};
}import { eventHandlersByType, hasProp, hasAnyProp } from "jsx-ast-utils";
function checkElementInteractivity(node) {
// Check for mouse interactions
const hasMouseEvents = hasAnyProp(node.attributes, eventHandlersByType.mouse);
// Check for keyboard interactions
const hasKeyboardEvents = hasAnyProp(node.attributes, eventHandlersByType.keyboard);
// Check for form interactions
const hasFormEvents = hasAnyProp(node.attributes, eventHandlersByType.form);
return {
isClickable: hasMouseEvents,
isKeyboardAccessible: hasKeyboardEvents,
isFormElement: hasFormEvents
};
}