Skip to content

Commit

Permalink
add no-create-record-rerender rule
Browse files Browse the repository at this point in the history
  • Loading branch information
runspired committed Sep 28, 2024
1 parent 308fa59 commit e49c9f5
Show file tree
Hide file tree
Showing 2 changed files with 304 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// @ts-check

const messageId = 'noCreateRecordRerender';
const createRecordExpression = 'CallExpression[callee.property.name="createRecord"]';
const emberObjectExtendExpression = 'CallExpression[callee.property.name="extend"]';
const forbiddenParentMethodKeyNames = [
'init',
'initRecord', // legacy modals
'didReceiveAttrs',
'willRender',
'didInsertElement',
'didRender',
'didUpdateAttrs',
'willUpdate',
'willDestroyElement',
'willClearRender',
'didDestroyElement',
];

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'problem',
docs: {
description: 'Disallow use of `store.createRecord` in getters, constructors, and class properties',
category: 'Possible Errors',
recommended: true,
url: 'https://ember-luna.com/docs/5-patterns/best-practices/general#dontusecreaterecordinsideconstructorsgettersorclassproperties',
},
messages: {
[messageId]:
'Cannot call `store.createRecord` in {{location}}. Calling `store.createRecord` inside constructors, getters, and class properties can cause issues with re-renders.',
},
},

create(context) {
return {
/**
* Handle class constructor
* @param {import('eslint').Rule.Node} node
*/
[`MethodDefinition[kind="constructor"] ${createRecordExpression}`](node) {
const maybeParentFunction = getParentFunction(node);
if (maybeParentFunction && !parentFunctionIsConstructor(maybeParentFunction)) {
return;
}
context.report({
node,
messageId,
data: { location: 'a constructor' },
});
},

/**
* Handle class getter
* @param {import('eslint').Rule.Node} node
*/
[`MethodDefinition[kind="get"] ${createRecordExpression}`](node) {
context.report({
node,
messageId,
data: { location: 'a getter' },
});
},

/**
* Handle class property initializer
* @param {import('eslint').Rule.Node} node
*/
[`PropertyDefinition ${createRecordExpression}`](node) {
if (getParentFunction(node)) {
return;
}
context.report({
node,
messageId,
data: { location: 'a class property initializer' },
});
},

/**
* Handle lifecycle hooks in a class
* @param {import('eslint').Rule.Node} node
*/
[`MethodDefinition[key.name=/${forbiddenParentMethodKeyNames.join('|')}/] FunctionExpression ${createRecordExpression}`](
node,
) {
const maybeParentFunction = getParentFunction(node);
if (maybeParentFunction && !parentFunctionIsInit(maybeParentFunction)) {
return;
}
context.report({
node,
messageId,
data: { location: 'a lifecycle hook' },
});
},

/**
* Handle the init method in an EmberObject
* @param {import('eslint').Rule.Node} node
*/
[`${emberObjectExtendExpression} Property[key.name=/${forbiddenParentMethodKeyNames.join('|')}/] FunctionExpression ${createRecordExpression}`](
node,
) {
const maybeParentFunction = getParentFunction(node);
if (maybeParentFunction && !parentFunctionIsInit(maybeParentFunction)) {
return;
}
context.report({
node,
messageId,
data: { location: 'a lifecycle hook' },
});
},

/**
* Handle a property initializer in an EmberObject
* @param {import('eslint').Rule.Node} node
*/
[`${emberObjectExtendExpression} Property > ${createRecordExpression}`](node) {
context.report({
node,
messageId,
data: { location: 'an object property initializer' },
});
},
};
},
};

function getParentFunction(/** @type {import('eslint').Rule.Node} */ node) {
if (node.parent) {
if (node.parent.type === 'ArrowFunctionExpression' || node.parent.type === 'FunctionExpression') {
return node.parent;
} else if (node.parent.type === 'ClassBody') {
return null;
}
return getParentFunction(node.parent);
}
return null;
}

/**
*
* @param {import('eslint').Rule.Node} maybeParentFunction
* @returns {boolean}
*/
function parentFunctionIsConstructor(maybeParentFunction) {
return 'kind' in maybeParentFunction.parent && maybeParentFunction.parent.kind === 'constructor';
}

/**
*
* @param {import('eslint').Rule.Node} maybeParentFunction
* @returns {boolean}
*/
function parentFunctionIsInit(maybeParentFunction) {
return (
'key' in maybeParentFunction.parent &&
maybeParentFunction.parent.key.type === 'Identifier' &&
forbiddenParentMethodKeyNames.includes(maybeParentFunction.parent.key.name)
);
}
140 changes: 140 additions & 0 deletions packages/eslint-plugin-ember-data/tests/no-create-record-rerender.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
// @ts-nocheck
const rule = require('../rules/no-create-record-rerender');
const RuleTester = require('eslint').RuleTester;

const eslintTester = new RuleTester({
parser: require.resolve('@babel/eslint-parser'),
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
requireConfigFile: false,
babelOptions: {
babelrc: false,
plugins: [['@babel/plugin-proposal-decorators', { decoratorsBeforeExport: true }]],
},
},
});

eslintTester.run('no-create-record-rerender', rule, {
valid: [
{
code: `
export default class MyComponent extends Component {
@service store;
@action newThing() {
this.newThing = this.store.createRecord('thing');
}
}
`,
},
{
code: `
export default class MyComponent extends Component {
@service store;
newThing = () => {
this.store.createRecord('thing');
}
}
`,
},
{
code: `
export default class MyComponent extends Component {
@service store;
constructor() {
this.makeModel = () => this.store.createRecord('thing');
}
}
`,
},
{
code: `
export default Component.extend({
store: service(),
init() {
this.makeModel = () => this.store.createRecord('thing');
}
})
`,
},
{
code: `
const pojo = {
model: this.store.createRecord('thing')
};
`,
},
{
code: `
const pojo = {
init() {
this.model = this.store.createRecord('thing');
}
};
`,
},
],
invalid: [
{
code: `
export default class MyComponent extends Component {
@service store;
constructor() {
this.model = this.store.createRecord('thing');
}
}
`,
errors: [
'Cannot call `store.createRecord` in a constructor. Calling `store.createRecord` inside constructors, getters, and class properties can cause issues with re-renders.',
],
},
{
code: `
export default class MyComponent extends Component {
@service store;
get myModel() {
return this.store.createRecord('thing');
}
}
`,
errors: [
'Cannot call `store.createRecord` in a getter. Calling `store.createRecord` inside constructors, getters, and class properties can cause issues with re-renders.',
],
},
{
code: `
export default class MyComponent extends Component {
@service store;
model = this.store.createRecord('thing');
}
`,
errors: [
'Cannot call `store.createRecord` in a class property initializer. Calling `store.createRecord` inside constructors, getters, and class properties can cause issues with re-renders.',
],
},
{
code: `
export default Component.extend({
store: service(),
init() {
this.model = this.store.createRecord('foo');
}
})
`,
errors: [
'Cannot call `store.createRecord` in a lifecycle hook. Calling `store.createRecord` inside constructors, getters, and class properties can cause issues with re-renders.',
],
},
{
code: `
export default Component.extend({
store: service(),
model: this.store.createRecord('foo')
})
`,
errors: [
'Cannot call `store.createRecord` in an object property initializer. Calling `store.createRecord` inside constructors, getters, and class properties can cause issues with re-renders.',
],
},
],
});

0 comments on commit e49c9f5

Please sign in to comment.