or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

ast-helpers.mdconfiguration.mdformatters.mdindex.mdlinter.mdrule-development.mdrules.md
tile.json

ast-helpers.mddocs/

AST Helpers

Utilities for analyzing and manipulating Handlebars AST nodes during rule execution, providing semantic information and pattern matching capabilities.

Capabilities

AST Node Information

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;
}

Node Pattern Matching

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;
}

Scope Information

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[];
}

Element Analysis

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;
}

Path Expression Analysis

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
          });
        }
      }
    };
  }
}

Node Location Utilities

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;
}