Utilities for analyzing and manipulating Handlebars AST nodes during rule execution, providing semantic information and pattern matching capabilities.
Comprehensive utilities for analyzing Handlebars AST nodes and extracting semantic information.
/**
* Collection of utilities for analyzing AST nodes
* Provides semantic information beyond basic AST structure
*/
class ASTHelpers {
/**
* Check if comment node is a template-lint configuration directive
* @param node - HTML comment statement node
* @returns true if comment contains template-lint directives
*/
static isConfigurationHtmlComment(node: CommentStatement): boolean;
/**
* Check if comment node is a regular HTML comment (not configuration)
* @param node - HTML comment statement node
* @returns true if comment is not a template-lint directive
*/
static isNonConfigurationHtmlComment(node: CommentStatement): boolean;
/**
* Check if node is an 'if' helper invocation
* @param node - Mustache, block, or sub-expression node
* @returns true if node represents {{if}} or {{#if}}
*/
static isIf(node: MustacheStatement | BlockStatement | SubExpression): boolean;
/**
* Check if node is an 'unless' helper invocation
* @param node - Mustache, block, or sub-expression node
* @returns true if node represents {{unless}} or {{#unless}}
*/
static isUnless(node: MustacheStatement | BlockStatement | SubExpression): boolean;
/**
* Check if node is an 'each' helper invocation
* @param node - Mustache, block, or sub-expression node
* @returns true if node represents {{#each}}
*/
static isEach(node: MustacheStatement | BlockStatement | SubExpression): boolean;
/**
* Check if node is an 'each-in' helper invocation
* @param node - Mustache, block, or sub-expression node
* @returns true if node represents {{#each-in}}
*/
static isEachIn(node: MustacheStatement | BlockStatement | SubExpression): boolean;
/**
* Check if node is a 'with' helper invocation
* @param node - Mustache, block, or sub-expression node
* @returns true if node represents {{#with}}
*/
static isWith(node: MustacheStatement | BlockStatement | SubExpression): boolean;
/**
* Check if element node represents an interactive element
* @param node - Element AST node
* @returns true if element is interactive (button, input, a, etc.)
*/
static isInteractiveElement(node: ElementNode): boolean;
/**
* Check if element uses angle bracket component syntax
* @param node - Element AST node
* @returns true if element is an angle bracket component
*/
static isAngleBracketComponent(node: ElementNode): boolean;
/**
* Check if node represents a curly component invocation
* @param node - Block or mustache statement node
* @returns true if node is a curly component invocation
*/
static isCurlyComponentInvocation(node: BlockStatement | MustacheStatement): boolean;
/**
* Check if name follows dasherized component naming convention
* @param name - Component or helper name
* @returns true if name is properly dasherized
*/
static isDasherizedComponentName(name: string): boolean;
/**
* Check if element has a specific parent tag
* @param node - Current element node
* @param parentTag - Tag name to check for
* @returns true if element has specified parent tag
*/
static hasParentTag(node: ElementNode, parentTag: string): boolean;
/**
* Get scope information for current template context
* @param node - AST node to analyze
* @returns Scope information including available variables
*/
static getScope(node: ASTNode): ScopeInfo;
/**
* Check if element has any of the specified attributes
* @param node - Element node to check
* @param attrNames - Array of attribute names to look for
* @returns true if element has any of the specified attributes
*/
static hasAnyAttribute(node: ElementNode, attrNames: string[]): boolean;
/**
* Check if element has a specific attribute
* @param node - Element node to check
* @param attrName - Attribute name to look for
* @returns true if element has the specified attribute
*/
static hasAttribute(node: ElementNode, attrName: string): boolean;
/**
* Find and return a specific attribute node
* @param node - Element node to search
* @param attrName - Attribute name to find
* @returns Attribute node if found, undefined otherwise
*/
static findAttribute(node: ElementNode, attrName: string): AttrNode | undefined;
/**
* Check if node is a control flow helper (if, unless, each, etc.)
* @param node - AST node to check
* @returns true if node is a control flow helper
*/
static isControlFlowHelper(node: ASTNode): boolean;
}Sophisticated pattern matching utilities for AST nodes.
/**
* Advanced pattern matching for AST nodes
* Based on jscodeshift's matching capabilities
*/
class NodeMatcher {
/**
* Match a node against a pattern or array of patterns
* @param testNode - Node to test against patterns
* @param referenceNode - Pattern(s) to match against
* @returns true if node matches any of the patterns
*/
static match(testNode: ASTNode, referenceNode: ASTNode | ASTNode[]): boolean;
/**
* Match node using a predicate function
* @param testNode - Node to test
* @param predicate - Function that returns boolean for match
* @returns true if predicate returns true for the node
*/
static matchFunction(testNode: ASTNode, predicate: (node: ASTNode) => boolean): boolean;
/**
* Deep match node properties recursively
* @param haystack - Node to search in
* @param needle - Pattern to find
* @returns true if needle pattern exists in haystack
*/
static matchNode(haystack: ASTNode, needle: Partial<ASTNode>): boolean;
}Template scope and variable analysis.
/**
* Template scope analysis for variable availability
*/
class Scope {
constructor(options: ScopeOptions): Scope;
/**
* Check if variable is available in current scope
* @param name - Variable name to check
* @returns true if variable is in scope
*/
hasBinding(name: string): boolean;
/**
* Get all available bindings in current scope
* @returns Array of available variable names
*/
getBindings(): string[];
/**
* Create child scope with additional bindings
* @param bindings - Additional variable bindings
* @returns New scope with extended bindings
*/
extend(bindings: string[]): Scope;
}
interface ScopeOptions {
/** Parent scope */
parent?: Scope;
/** Initial variable bindings */
bindings?: string[];
}
interface ScopeInfo {
/** Available variable bindings */
bindings: string[];
/** Block parameters from enclosing blocks */
blockParams: string[];
/** Component arguments in scope */
componentArgs: string[];
/** Helper names available */
helpers: string[];
}Utilities for analyzing HTML elements and their properties.
/**
* Element-specific analysis utilities
*/
class ElementAnalysis {
/**
* Check if element has specific attribute
* @param element - Element node to check
* @param attributeName - Name of attribute to find
* @returns true if element has the attribute
*/
static hasAttribute(element: ElementNode, attributeName: string): boolean;
/**
* Get attribute value from element
* @param element - Element node to search
* @param attributeName - Name of attribute to get
* @returns Attribute value or null if not found
*/
static getAttributeValue(element: ElementNode, attributeName: string): string | null;
/**
* Check if element has specific class
* @param element - Element node to check
* @param className - Class name to find
* @returns true if element has the class
*/
static hasClass(element: ElementNode, className: string): boolean;
/**
* Get all classes from element
* @param element - Element node to analyze
* @returns Array of class names
*/
static getClasses(element: ElementNode): string[];
/**
* Check if element is a void element (self-closing)
* @param tagName - Element tag name
* @returns true if element is void (br, img, input, etc.)
*/
static isVoidElement(tagName: string): boolean;
/**
* Check if element should be self-closing
* @param element - Element node to check
* @returns true if element should use self-closing syntax
*/
static shouldBeSelfClosing(element: ElementNode): boolean;
}Utilities for analyzing Handlebars path expressions and helper calls.
/**
* Path expression and helper analysis
*/
class PathAnalysis {
/**
* Check if path expression is a helper call
* @param path - Path expression node
* @returns true if path represents a helper
*/
static isHelper(path: PathExpression): boolean;
/**
* Check if path expression is a specific helper
* @param path - Path expression node
* @param helperName - Name of helper to check for
* @returns true if path is the specified helper
*/
static isHelperNamed(path: PathExpression, helperName: string): boolean;
/**
* Check if path is a component reference
* @param path - Path expression node
* @returns true if path references a component
*/
static isComponent(path: PathExpression): boolean;
/**
* Check if path uses 'this' reference
* @param path - Path expression node
* @returns true if path starts with 'this.'
*/
static isThisPath(path: PathExpression): boolean;
/**
* Get the root identifier of a path
* @param path - Path expression node
* @returns Root identifier name
*/
static getRootIdentifier(path: PathExpression): string;
/**
* Check if path is a block parameter reference
* @param path - Path expression node
* @param blockParams - Available block parameters
* @returns true if path references a block parameter
*/
static isBlockParam(path: PathExpression, blockParams: string[]): boolean;
}Usage Examples:
import { ASTHelpers, NodeMatcher, Scope } from "ember-template-lint";
// Custom rule using AST helpers
class MyCustomRule extends Rule {
visitor() {
return {
ElementNode(node) {
// Check if element is interactive
if (ASTHelpers.isInteractiveElement(node)) {
// Verify it has proper accessibility attributes
if (!ElementAnalysis.hasAttribute(node, 'aria-label') &&
!ElementAnalysis.hasAttribute(node, 'aria-labelledby')) {
this.log({
message: 'Interactive elements must have accessible labels',
node
});
}
}
// Check for angle bracket components
if (ASTHelpers.isAngleBracketComponent(node)) {
// Validate component naming
if (!ASTHelpers.isDasherizedComponentName(node.tag)) {
this.log({
message: 'Component names should be dasherized',
node
});
}
}
},
BlockStatement(node) {
// Check for specific helpers
if (ASTHelpers.isEach(node)) {
// Validate each block has key
const hasKey = node.hash.pairs.some(pair => pair.key === 'key');
if (!hasKey) {
this.log({
message: 'Each blocks should have a key attribute',
node
});
}
}
},
MustacheStatement(node) {
// Check for implicit this usage
if (PathAnalysis.isThisPath(node.path)) {
this.log({
message: 'Avoid implicit this references',
node
});
}
}
};
}
}
// Using NodeMatcher for complex patterns
class PatternMatchingRule extends Rule {
visitor() {
return {
ElementNode(node) {
// Match specific element patterns
const buttonPattern = {
type: 'ElementNode',
tag: 'button'
};
const divWithClassPattern = {
type: 'ElementNode',
tag: 'div',
attributes: [
{ name: 'class' }
]
};
if (NodeMatcher.match(node, [buttonPattern, divWithClassPattern])) {
// Handle matched elements
this.processMatchedElement(node);
}
// Using predicate functions
const isFormElement = (n) =>
['input', 'select', 'textarea'].includes(n.tag);
if (NodeMatcher.matchFunction(node, isFormElement)) {
this.validateFormElement(node);
}
}
};
}
}
// Using Scope for variable analysis
class ScopeAwareRule extends Rule {
visitor() {
let currentScope = new Scope();
return {
BlockStatement: {
enter(node) {
// Create new scope for block parameters
if (node.program.blockParams.length > 0) {
currentScope = currentScope.extend(node.program.blockParams);
}
},
exit(node) {
// Restore parent scope
if (node.program.blockParams.length > 0) {
currentScope = currentScope.parent;
}
}
},
PathExpression(node) {
const rootId = PathAnalysis.getRootIdentifier(node);
if (!currentScope.hasBinding(rootId) &&
!PathAnalysis.isHelper(node) &&
!PathAnalysis.isThisPath(node)) {
this.log({
message: `"${rootId}" is not defined in current scope`,
node
});
}
}
};
}
}Utilities for working with AST node source locations.
/**
* Source location and position utilities
*/
class LocationUtils {
/**
* Get source text for a node
* @param node - AST node
* @param source - Original template source
* @returns Source text for the node
*/
static getNodeText(node: ASTNode, source: string): string;
/**
* Get line and column from character offset
* @param source - Template source
* @param offset - Character offset
* @returns Line and column position
*/
static offsetToPosition(source: string, offset: number): Position;
/**
* Get character offset from line and column
* @param source - Template source
* @param line - Line number (1-based)
* @param column - Column number (0-based)
* @returns Character offset
*/
static positionToOffset(source: string, line: number, column: number): number;
/**
* Create source location from start and end positions
* @param start - Start position
* @param end - End position
* @returns Source location object
*/
static createLocation(start: Position, end: Position): SourceLocation;
}
interface Position {
line: number; // 1-based line number
column: number; // 0-based column number
}
interface SourceLocation {
start: Position;
end: Position;
}