Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/config/base.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const plugin = require('../index');
const emberEslintParser = require('ember-eslint-parser');
const { getEmber71BuiltInKeywords } = require('../utils/ember71-built-in-keywords');

module.exports = [
{
Expand All @@ -14,6 +15,9 @@ module.exports = [
files: ['**/*.{gts,gjs}'],
languageOptions: {
parser: emberEslintParser,
globals: {
...getEmber71BuiltInKeywords(),
},
},
processor: 'ember/noop',
},
Expand Down
7 changes: 7 additions & 0 deletions lib/recommended.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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: {
Expand All @@ -47,6 +51,9 @@ export const gts = {
extraFileExtensions: ['.gts'],
},
// parser options should be supplied in the consuming project
globals: {
...getEmber71BuiltInKeywords(),
},
},
processor: 'ember/noop',
rules: {
Expand Down
74 changes: 74 additions & 0 deletions lib/utils/ember71-built-in-keywords.js
Original file line number Diff line number Diff line change
@@ -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,
};
65 changes: 65 additions & 0 deletions tests/lib/utils/ember71-built-in-keywords-test.js
Original file line number Diff line number Diff line change
@@ -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({});
});
});
Loading