UNPKG

eslint-plugin-ember

Version:
145 lines (124 loc) 5.23 kB
'use strict'; const emberSourceVersion = require('../utils/ember-source-version'); //------------------------------------------------------------------------------ // Mapping from tracked-built-ins exports to @ember/reactive/collections exports //------------------------------------------------------------------------------ const TRACKED_BUILT_INS_MAPPING = { TrackedArray: 'trackedArray', TrackedObject: 'trackedObject', TrackedMap: 'trackedMap', TrackedWeakMap: 'trackedWeakMap', TrackedSet: 'trackedSet', TrackedWeakSet: 'trackedWeakSet', }; const TRACKED_BUILT_INS_MODULE = 'tracked-built-ins'; const EMBER_REACTIVE_MODULE = '@ember/reactive/collections'; const ERROR_MESSAGE_IMPORT = 'Use imports from `@ember/reactive/collections` instead of `tracked-built-ins`.'; //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ /** @type {import('eslint').Rule.RuleModule} */ module.exports = { meta: { type: 'suggestion', docs: { description: 'enforce usage of `@ember/reactive/collections` imports instead of `tracked-built-ins`', category: 'Ember Octane', recommended: false, url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/no-tracked-built-ins.md', }, fixable: 'code', schema: [], messages: { import: ERROR_MESSAGE_IMPORT, newExpression: 'Use `{{newName}}(...)` instead of `new {{oldName}}(...)`. The `@ember/reactive/collections` utilities do not use `new`.', }, }, ERROR_MESSAGE_IMPORT, create(context) { // Only report when ember-source >= 6.8 (which provides @ember/reactive/collections) if (!emberSourceVersion.isEmberSourceVersionAtLeast(6, 8)) { return {}; } // Track which imported identifiers map to tracked-built-ins classes // so we can fix `new TrackedArray(...)` → `trackedArray(...)` const trackedIdentifiers = new Map(); return { ImportDeclaration(node) { if (node.source.value !== TRACKED_BUILT_INS_MODULE) { return; } context.report({ node, messageId: 'import', fix(fixer) { const specifiers = node.specifiers; // Only autofix named imports we know how to map const namedSpecifiers = specifiers.filter( (s) => s.type === 'ImportSpecifier' && s.imported.name in TRACKED_BUILT_INS_MAPPING ); // If there's a default import or unknown named imports, we can't fully autofix const hasDefault = specifiers.some((s) => s.type === 'ImportDefaultSpecifier'); const unknownNamed = specifiers.filter( (s) => s.type === 'ImportSpecifier' && !(s.imported.name in TRACKED_BUILT_INS_MAPPING) ); if (hasDefault || unknownNamed.length > 0 || namedSpecifiers.length === 0) { return null; } const newSpecifiers = namedSpecifiers.map((s) => { const newName = TRACKED_BUILT_INS_MAPPING[s.imported.name]; if (s.local.name !== s.imported.name) { // Has alias: `import { TrackedArray as TA }` → `import { trackedArray as TA }` return `${newName} as ${s.local.name}`; } return newName; }); const newImport = `import { ${newSpecifiers.join(', ')} } from '${EMBER_REACTIVE_MODULE}';`; return fixer.replaceText(node, newImport); }, }); // Register the local names for NewExpression tracking for (const specifier of node.specifiers) { if ( specifier.type === 'ImportSpecifier' && specifier.imported.name in TRACKED_BUILT_INS_MAPPING ) { const isAliased = specifier.local.name !== specifier.imported.name; trackedIdentifiers.set(specifier.local.name, { newName: TRACKED_BUILT_INS_MAPPING[specifier.imported.name], isAliased, }); } } }, NewExpression(node) { if (node.callee.type === 'Identifier' && trackedIdentifiers.has(node.callee.name)) { const oldName = node.callee.name; const { newName, isAliased } = trackedIdentifiers.get(oldName); context.report({ node, messageId: 'newExpression', data: { oldName, newName }, fix(fixer) { const sourceCode = context.sourceCode; const newKeyword = sourceCode.getFirstToken(node); const calleeToken = sourceCode.getTokenAfter(newKeyword); const fixes = [ // Remove the `new` keyword and any whitespace up to the callee fixer.removeRange([newKeyword.range[0], calleeToken.range[0]]), ]; // Only rename the callee if it's not aliased if (!isAliased) { fixes.push(fixer.replaceText(node.callee, newName)); } return fixes; }, }); } }, }; }, };