diff --git a/lib/config/base.js b/lib/config/base.js index c6bdf52381..e4a6256da4 100644 --- a/lib/config/base.js +++ b/lib/config/base.js @@ -1,5 +1,6 @@ const plugin = require('../index'); const emberEslintParser = require('ember-eslint-parser'); +const { getEmber71BuiltInKeywords } = require('../utils/ember71-built-in-keywords'); module.exports = [ { @@ -14,6 +15,9 @@ module.exports = [ files: ['**/*.{gts,gjs}'], languageOptions: { parser: emberEslintParser, + globals: { + ...getEmber71BuiltInKeywords(), + }, }, processor: 'ember/noop', }, diff --git a/lib/recommended.mjs b/lib/recommended.mjs index 4e0a095b2c..e47d1cfb17 100644 --- a/lib/recommended.mjs +++ b/lib/recommended.mjs @@ -4,6 +4,7 @@ import gjsRules from './recommended-rules-gjs.js'; import gtsRules from './recommended-rules-gts.js'; import emberParser from 'ember-eslint-parser'; import emberHbsParser from 'ember-eslint-parser/hbs'; +import { getEmber71BuiltInKeywords } from './utils/ember71-built-in-keywords.js'; export const plugin = emberPlugin; export const parser = emberParser; @@ -29,6 +30,9 @@ export const gjs = { ecmaVersion: 'latest', // babel config options should be supplied in the consuming project }, + globals: { + ...getEmber71BuiltInKeywords(), + }, }, processor: 'ember/noop', rules: { @@ -47,6 +51,9 @@ export const gts = { extraFileExtensions: ['.gts'], }, // parser options should be supplied in the consuming project + globals: { + ...getEmber71BuiltInKeywords(), + }, }, processor: 'ember/noop', rules: { diff --git a/lib/utils/ember71-built-in-keywords.js b/lib/utils/ember71-built-in-keywords.js new file mode 100644 index 0000000000..964532a3be --- /dev/null +++ b/lib/utils/ember71-built-in-keywords.js @@ -0,0 +1,74 @@ +'use strict'; + +/** + * Built-in template keywords shipped from `ember-source` 7.1.0 + * (RFCs 389, 470, 560, 561, 562, 997, 998, 999, 1000). They can be referenced + * in strict-mode (`.gjs` / `.gts`) templates without an explicit import, so + * ESLint's `no-undef` needs to know about them. + * + * Mirrors Glint's `KeywordsForEmber71` gate in + * `@glint/ember-tsc/types/-private/dsl/globals.d.ts`. + */ +const KEYWORDS = { + and: 'readonly', + array: 'readonly', + element: 'readonly', + eq: 'readonly', + fn: 'readonly', + gt: 'readonly', + gte: 'readonly', + hash: 'readonly', + lt: 'readonly', + lte: 'readonly', + neq: 'readonly', + not: 'readonly', + on: 'readonly', + or: 'readonly', +}; + +/** + * Returns the 7.1 built-in keywords as an ESLint globals map when the supplied + * `ember-source` version is >= 7.1.0. Returns an empty object otherwise. + * + * Pure — accepts the version string instead of reading the filesystem so it + * is trivially testable. + */ +function ember71BuiltInKeywordsForVersion(version) { + if (typeof version !== 'string') { + return {}; + } + + const [major, minor] = version.split('.').map(Number); + const isAtLeast71 = major > 7 || (major === 7 && minor >= 1); + + return isAtLeast71 ? KEYWORDS : {}; +} + +/** + * Probe the consumer's installed `ember-source` to decide whether the 7.1 + * built-in keywords should be exposed as globals. + * + * Glint performs the equivalent check at the type level by probing the value + * exports of `@ember/helper`; we can't do that from a Node-evaluated ESLint + * config, so we read `ember-source/package.json` and compare semver instead. + * + * Returns an empty object if `ember-source` is not resolvable, or if its + * version is older than 7.1.0, so projects without it (or pre-7.1 projects) + * keep their existing lint behaviour. + */ +function getEmber71BuiltInKeywords() { + try { + // ember-source is a peer of consumers, not of this plugin, so neither the + // `n/no-missing-require` nor `import/extensions` rules can resolve it here. + // eslint-disable-next-line n/no-missing-require, import/extensions, import/no-unresolved + const { version } = require('ember-source/package.json'); + return ember71BuiltInKeywordsForVersion(version); + } catch { + return {}; + } +} + +module.exports = { + getEmber71BuiltInKeywords, + ember71BuiltInKeywordsForVersion, +}; diff --git a/tests/lib/utils/ember71-built-in-keywords-test.js b/tests/lib/utils/ember71-built-in-keywords-test.js new file mode 100644 index 0000000000..920490d2a8 --- /dev/null +++ b/tests/lib/utils/ember71-built-in-keywords-test.js @@ -0,0 +1,65 @@ +'use strict'; + +const { + ember71BuiltInKeywordsForVersion, + getEmber71BuiltInKeywords, +} = require('../../../lib/utils/ember71-built-in-keywords'); + +const EXPECTED_KEYWORDS = [ + 'and', + 'array', + 'element', + 'eq', + 'fn', + 'gt', + 'gte', + 'hash', + 'lt', + 'lte', + 'neq', + 'not', + 'on', + 'or', +]; + +describe('ember71BuiltInKeywordsForVersion', () => { + it('returns the 7.1 built-in keywords as readonly globals for ember-source >= 7.1.0', () => { + const globals = ember71BuiltInKeywordsForVersion('7.1.0'); + + expect(Object.keys(globals).sort()).toEqual(EXPECTED_KEYWORDS); + + for (const keyword of EXPECTED_KEYWORDS) { + expect(globals[keyword]).toBe('readonly'); + } + }); + + it('returns the keywords for pre-release ember-source 7.1 builds (e.g. 7.1.0-beta.1)', () => { + expect(Object.keys(ember71BuiltInKeywordsForVersion('7.1.0-beta.1')).sort()).toEqual( + EXPECTED_KEYWORDS + ); + }); + + it('returns the keywords for ember-source > 7.1.0', () => { + expect(Object.keys(ember71BuiltInKeywordsForVersion('8.0.0-beta.1')).sort()).toEqual( + EXPECTED_KEYWORDS + ); + }); + + it('returns an empty object for ember-source < 7.1.0', () => { + expect(ember71BuiltInKeywordsForVersion('7.0.5')).toEqual({}); + expect(ember71BuiltInKeywordsForVersion('6.4.0')).toEqual({}); + }); + + it('returns an empty object for non-string input', () => { + expect(ember71BuiltInKeywordsForVersion(undefined)).toEqual({}); + expect(ember71BuiltInKeywordsForVersion(null)).toEqual({}); + }); +}); + +describe('getEmber71BuiltInKeywords', () => { + it('returns an empty object when ember-source is not installed in this repo', () => { + // eslint-plugin-ember does not depend on ember-source, so the require() + // inside getEmber71BuiltInKeywords() throws and falls through to {}. + expect(getEmber71BuiltInKeywords()).toEqual({}); + }); +});