-
Notifications
You must be signed in to change notification settings - Fork 34
Updated no-broken-super-chain
rule to account for more lifecycle hooks …
#94
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 3 commits
a8b7a68
abdad6d
56dcd1b
1a8aad3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,95 +1,106 @@ | ||
/** | ||
* @fileOverview Prevent the absence of this._super() in init() calls or the use of this prior to this._super() | ||
* @fileOverview Prevent the absence of `this._super(...arguments)` in calls to various lifecycle hooks | ||
*/ | ||
|
||
const MESSAGES = { | ||
noSuper: '\'this._super(...arguments)\' must be called in init()', | ||
noThisBeforeSuper: 'Must call \'this._super(...arguments)\' before accessing `this`', | ||
tooManySupers: 'Only call `this._super(...arguments)` once per init()' | ||
noSuper: '`this._super(...arguments)` must be called in', | ||
tooManySupers: 'Only call `this._super(...arguments)` once per lifecycle hook', | ||
argsNotPassedToSuper: '...arguments need to be passed to this._super() call' | ||
}; | ||
|
||
// TODO: Make this configurable | ||
const EMBER_MODULES_WITH_SUPER_CHAIN = { | ||
Component: true, | ||
Mixin: true, | ||
Route: true, | ||
Controller: true, | ||
View: true | ||
}; | ||
const LIFECYCLE_HOOKS = ['init', 'willDestroy', 'willDestroyElement', 'destroy']; | ||
|
||
/** | ||
* Determines if this is an init method in an extension of Ember[EMBER_MODULES_WITH_SUPER_CHAIN.*] | ||
* @param {Node} node | ||
*/ | ||
function isInit(node) { | ||
if (node.type === 'FunctionExpression' && node.parent && node.parent.key && node.parent.key.name === 'init') { | ||
function isExtend(node) { | ||
return node && node.callee && node.callee.property && node.callee.property.name === 'extend'; | ||
} | ||
|
||
function isSuperCall(lineWithinFn) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One more case we need to handle is: Foo.extend({
init() {
return this._super(...argument);
}
}); This ^ is valid, the linting rule should not complain about it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Great catch. Updated to: |
||
if (lineWithinFn.type !== 'MemberExpression') { | ||
return false; | ||
} | ||
|
||
let isSuperCall = false; | ||
|
||
if (lineWithinFn.object.type === 'ThisExpression') { | ||
isSuperCall = lineWithinFn.property.type === 'Identifier' && lineWithinFn.property.name === '_super'; | ||
} else if (lineWithinFn.object.type === 'MemberExpression') { | ||
isSuperCall = lineWithinFn.object.property.name === '_super'; | ||
} | ||
|
||
return isSuperCall; | ||
} | ||
|
||
function wereArgumentsPassedToSuper(expression) { | ||
const callee = expression.callee; | ||
|
||
if (node.parent.parent | ||
&& node.parent.parent.parent | ||
&& node.parent.parent.parent.callee | ||
&& node.parent.parent.parent.callee.object | ||
&& node.parent.parent.parent.callee.object.object | ||
&& node.parent.parent.parent.callee.object.object.name === 'Ember') { | ||
return (node.parent.parent.parent.callee.object.property | ||
&& EMBER_MODULES_WITH_SUPER_CHAIN[node.parent.parent.parent.callee.object.property.name]); | ||
} | ||
if (!expression || callee.type !== 'MemberExpression') { | ||
return; | ||
} | ||
|
||
return false; | ||
if (callee.property.name === '_super') { | ||
const firstArgumentToSuper = expression.arguments[0]; | ||
return firstArgumentToSuper && firstArgumentToSuper.type === 'SpreadElement' && firstArgumentToSuper.argument.name === 'arguments'; | ||
} else if (callee.property.name === 'apply') { | ||
const args = expression.arguments; | ||
return args.length === 2 && args[0].type === 'ThisExpression' && args[1].name === 'arguments'; | ||
} | ||
} | ||
|
||
module.exports = { | ||
meta: { | ||
docs: { | ||
description: 'Prevent the absence of `this._super(...arguments)` in `init()` calls or the use of `this` prior to `this._super()`', | ||
description: 'Prevent the absence of `this._super(...arguments)` in calls to various lifecycle hooks', | ||
category: 'Best Practices', | ||
recommended: true | ||
}, | ||
messages: MESSAGES | ||
}, | ||
create(context) { | ||
let initOverride = null; | ||
|
||
return { | ||
onCodePathStart(codePath, node) { | ||
if (isInit(node)) { | ||
initOverride = { | ||
superCalled: false, | ||
superCalledFirst: false | ||
}; | ||
} | ||
}, | ||
onCodePathEnd(codePath, node) { | ||
if (initOverride && isInit(node)) { // TODO: Maybe check against codepath.name | ||
if (!initOverride.superCalled) { | ||
context.report({ | ||
message: MESSAGES.noSuper, | ||
node | ||
}); | ||
} | ||
CallExpression(node) { | ||
if (isExtend(node)) { | ||
const extendedObjects = node.arguments.filter(arg => arg.type === 'ObjectExpression'); | ||
extendedObjects.forEach(extendedObj => { | ||
let superCount = 0; | ||
extendedObj.properties.forEach(property => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need to guard for I believe the following test should be added (which will fail ATM but pass if we filtered non ObjectExpressions): Foo.extend(SomeMixin, {
init() {
this._super(...arguments);
}
}); I'd suggest either of the following: extendedObjects = node.arguments.filter(arg => arg.type === 'ObjectExpression');
// or
extendedObjects.forEach(extendedObj => {
if (extendedObj.type !== 'ObjectExpression') { return; }
// ...snip...
}); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would also suggest adding a test case for this. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Updated, and added test. |
||
if (LIFECYCLE_HOOKS.includes(property.key.name)) { | ||
const propertyFnBody = property.value.body; | ||
|
||
initOverride = null; | ||
} | ||
return; | ||
}, | ||
'CallExpression:exit'(node) { | ||
if (initOverride) { | ||
const property = node.callee.property; | ||
if (property && property.type === 'Identifier' && property.name === '_super') { | ||
if (initOverride.superCalled) { | ||
context.report({ | ||
message: MESSAGES.tooManySupers, | ||
node | ||
}); | ||
} else { | ||
initOverride.superCalled = true; | ||
} | ||
} | ||
if (propertyFnBody && propertyFnBody.body) { | ||
let expression; | ||
propertyFnBody.body.forEach(expressionStatement => { | ||
expression = expressionStatement.type === 'ReturnStatement' ? expressionStatement.argument : expressionStatement.expression; | ||
if (expression.type === 'CallExpression') { | ||
const callee = expression.callee; | ||
if (isSuperCall(callee)) { | ||
if (!wereArgumentsPassedToSuper(expression)) { | ||
context.report({ | ||
node: expressionStatement, | ||
message: context.meta.messages.argsNotPassedToSuper | ||
}); | ||
} | ||
superCount++; | ||
} | ||
} | ||
}); | ||
|
||
if (superCount === 0) { | ||
context.report({ | ||
node: property, | ||
message: `${context.meta.messages.noSuper} ${property.key.name}` | ||
}); | ||
} else if (superCount > 1) { | ||
context.report({ | ||
node: property, | ||
message: context.meta.messages.tooManySupers | ||
}); | ||
} | ||
} | ||
} | ||
}); | ||
}); | ||
} | ||
}, | ||
'Program:exit'() { | ||
initOverride = null; | ||
} | ||
}; | ||
} | ||
}; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,7 +1,7 @@ | ||
const rule = require('../../../lib/rules/no-broken-super-chain'); | ||
const RuleTester = require('eslint').RuleTester; | ||
|
||
const { noSuper, tooManySupers } = rule.meta.messages; | ||
const { noSuper, tooManySupers, argsNotPassedToSuper } = rule.meta.messages; | ||
const ruleTester = new RuleTester({ | ||
parserOptions: { | ||
ecmaVersion: 6, | ||
|
@@ -23,6 +23,15 @@ ruleTester.run('no-broken-super-chain', rule, { | |
} | ||
});` | ||
}, | ||
{ | ||
code: ` | ||
export default Ember.Component.extend({ | ||
init() { | ||
this._super.apply(this, arguments); | ||
this.alias = this.concrete; | ||
}, | ||
});` | ||
}, | ||
{ | ||
code: ` | ||
export default Ember.Route.extend({ | ||
|
@@ -47,14 +56,6 @@ ruleTester.run('no-broken-super-chain', rule, { | |
} | ||
});` | ||
}, | ||
{ | ||
code: ` | ||
export default Ember.Service.extend({ | ||
init() { | ||
this.alias = this.concrete; | ||
} | ||
});` | ||
}, | ||
{ | ||
code: ` | ||
export default Ember.Component.extend({ | ||
|
@@ -82,9 +83,40 @@ ruleTester.run('no-broken-super-chain', rule, { | |
}); | ||
|
||
export default foo;` | ||
}, | ||
{ | ||
code: ` | ||
export default Ember.Component.extend(SomeMixin, { | ||
init() { | ||
this._super(...arguments); | ||
} | ||
});` | ||
}, | ||
{ | ||
code: ` | ||
export default Ember.Component.extend({ | ||
init() { | ||
return this._super(...arguments); | ||
} | ||
});` | ||
}, | ||
{ | ||
code: ` | ||
export default Foo.extend({ [lol]: function() {}});` | ||
} | ||
], | ||
invalid: [ | ||
{ | ||
code: ` | ||
export default Ember.Service.extend({ | ||
init() { | ||
this.alias = this.concrete; | ||
} | ||
});`, | ||
errors: [{ | ||
message: `${noSuper} init` | ||
}] | ||
}, | ||
{ | ||
code: ` | ||
export default Ember.Component.extend({ | ||
|
@@ -93,7 +125,40 @@ ruleTester.run('no-broken-super-chain', rule, { | |
} | ||
});`, | ||
errors: [{ | ||
message: noSuper | ||
message: `${noSuper} init` | ||
}] | ||
}, | ||
{ | ||
code: ` | ||
export default Ember.Component.extend({ | ||
destroy() { | ||
this.alias = this.concrete; | ||
} | ||
});`, | ||
errors: [{ | ||
message: `${noSuper} destroy` | ||
}] | ||
}, | ||
{ | ||
code: ` | ||
export default Ember.Component.extend({ | ||
willDestroy() { | ||
this.alias = this.concrete; | ||
} | ||
});`, | ||
errors: [{ | ||
message: `${noSuper} willDestroy` | ||
}] | ||
}, | ||
{ | ||
code: ` | ||
export default Ember.Component.extend({ | ||
willDestroyElement() { | ||
this.alias = this.concrete; | ||
} | ||
});`, | ||
errors: [{ | ||
message: `${noSuper} willDestroyElement` | ||
}] | ||
}, | ||
{ | ||
|
@@ -104,32 +169,36 @@ ruleTester.run('no-broken-super-chain', rule, { | |
} | ||
});`, | ||
errors: [{ | ||
message: noSuper | ||
message: `${noSuper} init` | ||
}] | ||
}, | ||
// TODO | ||
// { | ||
// code: ` | ||
// export default Ember.Component.extend({ | ||
// init() { | ||
// this._super(); // missing '...arguments' | ||
// this.alias = this.concrete; | ||
// } | ||
// });`, | ||
// }, | ||
// TODO | ||
// { | ||
// code: ` | ||
// export default Ember.Component.extend({ | ||
// init() { | ||
// this.alias = this.concrete; | ||
// this._super(...arguments); | ||
// } | ||
// });`, | ||
// errors: [{ | ||
// message: noThisBeforeSuper | ||
// }] | ||
// }, | ||
{ | ||
code: ` | ||
export default Ember.Component.extend({ | ||
init() { | ||
this._super(); // missing '...arguments' | ||
this.alias = this.concrete; | ||
} | ||
});`, | ||
errors: [{ | ||
message: argsNotPassedToSuper | ||
}] | ||
}, | ||
/** | ||
TODO | ||
{ | ||
code: ` | ||
export default Ember.Component.extend({ | ||
init() { | ||
this.alias = this.concrete; | ||
this._super(...arguments); | ||
} | ||
});`, | ||
errors: [{ | ||
message: noThisBeforeSuper | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let’s remove this TODO commented test. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When we add a rule about no this before super we will probably make it a new more focused rule anyways. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed TODO |
||
}] | ||
}, | ||
*/ | ||
{ | ||
code: ` | ||
export default Ember.Component.extend({ | ||
|
@@ -156,4 +225,4 @@ ruleTester.run('no-broken-super-chain', rule, { | |
// }] | ||
// } | ||
] | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@rwjblue to be 100% safe, I think we should add all life cycle hooks here. Otherwise, we have no guarantee that mixins will work as expected.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ya, makes sense. Do we have a canonical list?
I guess we can start with the ones listed in https://guides.emberjs.com/v2.16.0/components/the-component-lifecycle/...