-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
2 changed files
with
304 additions
and
0 deletions.
There are no files selected for viewing
164 changes: 164 additions & 0 deletions
164
packages/eslint-plugin-ember-data/src/rules/no-create-record-rerender.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
140
packages/eslint-plugin-ember-data/tests/no-create-record-rerender.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.', | ||
], | ||
}, | ||
], | ||
}); |