diff --git a/README.md b/README.md index 5cd3427637..d9e4c1425d 100644 --- a/README.md +++ b/README.md @@ -396,6 +396,7 @@ To disable a rule for an entire `.gjs`/`.gts` file, use a regular ESLint file-le | [no-duplicate-dependent-keys](docs/rules/no-duplicate-dependent-keys.md) | disallow repeating computed property dependent keys | ✅ | 🔧 | | | [no-incorrect-computed-macros](docs/rules/no-incorrect-computed-macros.md) | disallow incorrect usage of computed property macros | ✅ | 🔧 | | | [no-invalid-dependent-keys](docs/rules/no-invalid-dependent-keys.md) | disallow invalid dependent keys in computed properties | ✅ | 🔧 | | +| [no-legacy-computed](docs/rules/no-legacy-computed.md) | disallow use of legacy computed properties and computed macros | | | | | [no-side-effects](docs/rules/no-side-effects.md) | disallow unexpected side effects in computed properties | ✅ | | | | [no-volatile-computed-properties](docs/rules/no-volatile-computed-properties.md) | disallow volatile computed properties | ✅ | | | | [require-computed-macros](docs/rules/require-computed-macros.md) | require using computed property macros when possible | ✅ | 🔧 | | diff --git a/docs/rules/no-legacy-computed.md b/docs/rules/no-legacy-computed.md new file mode 100644 index 0000000000..1d46084bfe --- /dev/null +++ b/docs/rules/no-legacy-computed.md @@ -0,0 +1,40 @@ +# ember/no-legacy-computed + + + +Disallow legacy computed properties and computed macros. + +## Rule Details + +This rule disallows: + +- `computed` imported from `@ember/object` +- Any import from `@ember/object/computed` (for example `and`, `gt`, `sortBy`, and other macros) + +Use `@tracked` and native getters instead. + +## Examples + +Examples of **incorrect** code for this rule: + +```js +import { computed } from '@ember/object'; +``` + +```js +import { and, gt, sortBy } from '@ember/object/computed'; +``` + +Examples of **correct** code for this rule: + +```js +import { tracked } from '@glimmer/tracking'; + +class Example { + @tracked count = 0; + + get doubled() { + return this.count * 2; + } +} +``` diff --git a/lib/rules/no-legacy-computed.js b/lib/rules/no-legacy-computed.js new file mode 100644 index 0000000000..78afb2c0de --- /dev/null +++ b/lib/rules/no-legacy-computed.js @@ -0,0 +1,46 @@ +'use strict'; + +const ERROR_MESSAGE = + "Don't use legacy computed properties or computed macros. Prefer `@tracked` and native getters."; + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'disallow use of legacy computed properties and computed macros', + category: 'Computed Properties', + recommended: false, + url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/no-legacy-computed.md', + }, + fixable: null, + schema: [], + }, + + ERROR_MESSAGE, + + create(context) { + return { + ImportDeclaration(node) { + if (node.source.value === '@ember/object/computed') { + if (node.specifiers.length === 0) { + context.report({ node, message: ERROR_MESSAGE }); + return; + } + + for (const specifier of node.specifiers) { + context.report({ node: specifier, message: ERROR_MESSAGE }); + } + } + + if (node.source.value === '@ember/object') { + for (const specifier of node.specifiers) { + if (specifier.type === 'ImportSpecifier' && specifier.imported.name === 'computed') { + context.report({ node: specifier, message: ERROR_MESSAGE }); + } + } + } + }, + }; + }, +}; diff --git a/tests/lib/rules/no-legacy-computed.js b/tests/lib/rules/no-legacy-computed.js new file mode 100644 index 0000000000..8b24bd732d --- /dev/null +++ b/tests/lib/rules/no-legacy-computed.js @@ -0,0 +1,92 @@ +'use strict'; + +const rule = require('../../../lib/rules/no-legacy-computed'); +const RuleTester = require('eslint').RuleTester; + +const { ERROR_MESSAGE } = rule; + +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 2022, + sourceType: 'module', + }, +}); + +ruleTester.run('no-legacy-computed', rule, { + valid: [ + ` + import Component from '@glimmer/component'; + + export default class extends Component { + get doubled() { + return 2; + } + } + `, + ` + import { computed } from 'somewhere-else'; + computed(); + `, + ` + import { and, gt } from 'somewhere-else'; + and('a', 'b'); + gt('count', 2); + `, + ` + import EmberObject from '@ember/object'; + + export default EmberObject.extend({}); + `, + ], + invalid: [ + { + code: ` + import { computed } from '@ember/object'; + + class Example {} + `, + output: null, + errors: [{ message: ERROR_MESSAGE, type: 'ImportSpecifier' }], + }, + { + code: ` + import { computed as cp } from '@ember/object'; + + class Example {} + `, + output: null, + errors: [{ message: ERROR_MESSAGE, type: 'ImportSpecifier' }], + }, + { + code: ` + import { and } from '@ember/object/computed'; + + class Example {} + `, + output: null, + errors: [{ message: ERROR_MESSAGE, type: 'ImportSpecifier' }], + }, + { + code: ` + import { and, gt, sortBy as sort } from '@ember/object/computed'; + + class Example {} + `, + output: null, + errors: [ + { message: ERROR_MESSAGE, type: 'ImportSpecifier' }, + { message: ERROR_MESSAGE, type: 'ImportSpecifier' }, + { message: ERROR_MESSAGE, type: 'ImportSpecifier' }, + ], + }, + { + code: ` + import '@ember/object/computed'; + + class Example {} + `, + output: null, + errors: [{ message: ERROR_MESSAGE, type: 'ImportDeclaration' }], + }, + ], +});