or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

docs

component-rules.mdcomputed-property-rules.mdember-utils.mdindex.mdlegacy-configuration.mdmigration-rules.mdmodern-configuration.mdplugin-configuration.mdroute-rules.mdservice-rules.mdtest-rules.md
tile.json

computed-property-rules.mddocs/

Computed Property Rules

Rules governing computed properties, dependencies, and modern reactive patterns. These rules help maintain proper computed property usage and encourage migration to modern Ember reactivity patterns.

Capabilities

Computed Property Definition Rules

Rules for defining computed properties correctly and avoiding common mistakes.

/**
 * Disallow arrow function computed properties
 * Prevents 'this' binding issues in computed properties
 */
'ember/no-arrow-function-computed-properties': ESLintRule;

/**
 * Disallow computed properties in native classes
 * Encourages getters/setters over computed() in native classes
 */
'ember/no-computed-properties-in-native-classes': ESLintRule;

/**
 * Require computed property macros where applicable
 * Encourages use of built-in computed macros
 */
'ember/require-computed-macros': ESLintRule;

Usage Examples:

// ❌ Bad - arrow function computed property (triggers no-arrow-function-computed-properties)
export default Component.extend({
  fullName: computed('firstName', 'lastName', () => {
    return `${this.firstName} ${this.lastName}`; // 'this' is undefined
  })
});

// ✅ Good - regular function
export default Component.extend({
  fullName: computed('firstName', 'lastName', function() {
    return `${this.firstName} ${this.lastName}`;
  })
});

// ❌ Bad - computed() in native class (triggers no-computed-properties-in-native-classes)
export default class MyComponent extends Component {
  @computed('firstName', 'lastName')
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

// ✅ Good - native getter in native class
export default class MyComponent extends Component {
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

// ❌ Bad - manual computed when macro available (triggers require-computed-macros)
export default Component.extend({
  hasUsers: computed('users.[]', function() {
    return this.users.length > 0;
  })
});

// ✅ Good - using computed macro
export default Component.extend({
  hasUsers: notEmpty('users')
});

Computed Property Dependencies

Rules for managing computed property dependencies and avoiding common dependency issues.

/**
 * Require computed property dependencies
 * Ensures all dependencies are declared
 */
'ember/require-computed-property-dependencies': ESLintRule;

/**
 * Disallow duplicate dependent keys
 * Prevents redundant dependency declarations
 */
'ember/no-duplicate-dependent-keys': ESLintRule;

/**
 * Disallow deeply nested dependent keys with @each
 * Prevents performance issues with deep watching
 */
'ember/no-deeply-nested-dependent-keys-with-each': ESLintRule;

/**
 * Disallow invalid dependent keys
 * Ensures dependent keys are valid property paths
 */
'ember/no-invalid-dependent-keys': ESLintRule;

Usage Examples:

// ❌ Bad - missing dependencies (triggers require-computed-property-dependencies)
export default Component.extend({
  fullName: computed(function() {
    return `${this.firstName} ${this.lastName}`; // Missing 'firstName', 'lastName'
  })
});

// ✅ Good - all dependencies declared
export default Component.extend({
  fullName: computed('firstName', 'lastName', function() {
    return `${this.firstName} ${this.lastName}`;
  })
});

// ❌ Bad - duplicate dependencies (triggers no-duplicate-dependent-keys)
export default Component.extend({
  fullName: computed('firstName', 'lastName', 'firstName', function() {
    return `${this.firstName} ${this.lastName}`;
  })
});

// ✅ Good - unique dependencies
export default Component.extend({
  fullName: computed('firstName', 'lastName', function() {
    return `${this.firstName} ${this.lastName}`;
  })
});

// ❌ Bad - deeply nested @each (triggers no-deeply-nested-dependent-keys-with-each)
export default Component.extend({
  totalCost: computed('items.@each.variants.@each.price', function() {
    return this.items.reduce((sum, item) => {
      return sum + item.variants.reduce((variantSum, variant) => {
        return variantSum + variant.price;
      }, 0);
    }, 0);
  })
});

// ✅ Good - avoid deep @each nesting
export default Component.extend({
  totalCost: computed('items.@each.totalVariantCost', function() {
    return this.items.reduce((sum, item) => sum + item.totalVariantCost, 0);
  })
});

Computed Property Return Rules

Rules ensuring computed properties return values correctly.

/**
 * Require return from computed properties
 * Ensures computed properties always return a value
 */
'ember/require-return-from-computed': ESLintRule;

/**
 * Disallow volatile computed properties
 * Discourages volatile() usage in favor of proper dependencies
 */
'ember/no-volatile-computed-properties': ESLintRule;

Usage Examples:

// ❌ Bad - missing return statement (triggers require-return-from-computed)
export default Component.extend({
  processedData: computed('data', function() {
    this.data.forEach(item => {
      console.log(item);
    });
    // Missing return statement
  })
});

// ✅ Good - explicit return
export default Component.extend({
  processedData: computed('data', function() {
    return this.data.map(item => {
      return item.processed ? item : { ...item, processed: true };
    });
  })
});

// ❌ Bad - volatile computed property (triggers no-volatile-computed-properties)
export default Component.extend({
  currentTime: computed(function() {
    return new Date();
  }).volatile()
});

// ✅ Good - proper dependencies or native getter
export default class MyComponent extends Component {
  get currentTime() {
    return new Date(); // Native getter naturally recalculates
  }
}

Computed Property Expansion Rules

Rules for computed property dependent key expansion and optimization.

/**
 * Use brace expansion in dependent keys
 * Encourages compact dependency notation
 */
'ember/use-brace-expansion': ESLintRule;

Usage Examples:

// ❌ Bad - repetitive dependent keys (triggers use-brace-expansion)
export default Component.extend({
  hasContactInfo: computed(
    'user.email',
    'user.phone', 
    'user.address',
    function() {
      return !!(this.user.email || this.user.phone || this.user.address);
    }
  )
});

// ✅ Good - using brace expansion
export default Component.extend({
  hasContactInfo: computed('user.{email,phone,address}', function() {
    return !!(this.user.email || this.user.phone || this.user.address);
  })
});

Computed Property Analysis Utilities

Utility functions for analyzing computed properties in ESLint rules.

/**
 * Check if property is a computed property
 * @param node - Property node to check
 * @param importedEmberName - Name Ember is imported as
 * @param importedComputedName - Name computed is imported as
 * @param options - Additional options for detection
 */
function isComputedProp(
  node: ASTNode,
  importedEmberName: string,
  importedComputedName: string,
  options?: ComputedPropOptions
): boolean;

interface ComputedPropOptions {
  /** Include properties with suffixes like .volatile() */
  includeSuffix?: boolean;
  /** Include computed property macros like computed.and() */
  includeMacro?: boolean;
}

/**
 * Parse dependent keys from computed property
 * @param callExp - Computed property call expression
 * @returns Array of dependent key strings
 */
function parseDependentKeys(callExp: ASTNode): string[];

/**
 * Unwrap brace expressions in dependent keys
 * @param dependentKeys - Array of dependent key strings
 * @returns Expanded array of dependent keys
 */
function unwrapBraceExpressions(dependentKeys: string[]): string[];

/**
 * Check for duplicate dependent keys
 * @param callExp - Computed property call expression
 * @param importedEmberName - Name Ember is imported as
 * @param importedComputedName - Name computed is imported as
 * @returns True if duplicates found
 */
function hasDuplicateDependentKeys(
  callExp: ASTNode,
  importedEmberName: string,
  importedComputedName: string
): boolean;

Usage Examples:

// Using computed property utilities in custom ESLint rules
const emberUtils = require('eslint-plugin-ember').utils.ember;

// Check if property is computed
if (emberUtils.isComputedProp(property, 'Ember', 'computed', { 
  includeSuffix: true,
  includeMacro: true 
})) {
  // Analyze computed property
  const dependentKeys = emberUtils.parseDependentKeys(property.value);
  const expandedKeys = emberUtils.unwrapBraceExpressions(dependentKeys);
  
  if (emberUtils.hasDuplicateDependentKeys(property.value, 'Ember', 'computed')) {
    context.report({
      node: property,
      message: 'Computed property has duplicate dependent keys'
    });
  }
}

Migration from Computed Properties

Guidance for migrating from computed properties to modern reactive patterns.

/**
 * Modern alternatives to computed properties
 */
interface ModernReactivePatterns {
  /** Native getters for simple computed properties */
  nativeGetters: boolean;
  /** @tracked properties for reactive state */
  trackedProperties: boolean;
  /** @cached decorator for expensive computations */
  cachedDecorator: boolean;
  /** Auto-tracking for dependency management */
  autoTracking: boolean;
}

Migration Examples:

// ❌ Old - computed property in classic class
export default Component.extend({
  fullName: computed('firstName', 'lastName', function() {
    return `${this.firstName} ${this.lastName}`;
  })
});

// ✅ New - native getter with auto-tracking
export default class MyComponent extends Component {
  @tracked firstName;
  @tracked lastName;
  
  get fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

// ❌ Old - computed property with complex logic
export default Component.extend({
  expensiveValue: computed('data.[]', function() {
    return this.data.map(item => this.processItem(item));
  })
});

// ✅ New - cached decorator for expensive operations
import { cached } from '@glimmer/tracking';

export default class MyComponent extends Component {
  @tracked data;
  
  @cached
  get expensiveValue() {
    return this.data.map(item => this.processItem(item));
  }
}