-
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 1 commit
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 |
---|---|---|
|
@@ -3,93 +3,107 @@ | |
*/ | ||
|
||
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; | ||
} | ||
|
||
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]); | ||
} | ||
function wereArgumentsPassedToSuper(expression) { | ||
const callee = expression.callee; | ||
|
||
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'; | ||
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 would 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. +1 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. Yeah, not sure what I had in mind here |
||
} | ||
} | ||
|
||
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 or the use of `this` prior to `this._super()`', | ||
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 don't think we do this at the moment, do we? I don't see the code that does it at least... 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. good catch, also updated file description |
||
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)) { | ||
let superCount = 0; | ||
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. Seems like this variable should be initialized from inside the |
||
const extendedObjects = node.arguments; | ||
|
||
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; | ||
} | ||
} | ||
extendedObjects.forEach(extendedObj => { | ||
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; | ||
|
||
if (propertyFnBody && propertyFnBody.body) { | ||
let expression; | ||
property.value.body.body.forEach(fnBody => { | ||
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. This should likely use 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. Can you rename 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.
|
||
expression = fnBody.expression; | ||
if (expression.type === 'CallExpression') { | ||
const line = expression.callee; | ||
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.
|
||
if (isSuperCall(line)) { | ||
if (!wereArgumentsPassedToSuper(expression)) { | ||
context.report({ | ||
node: fnBody, | ||
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 | ||
}); | ||
} | ||
|
||
superCount = 0; | ||
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. This shouldn't be needed, we should initialize |
||
} | ||
} | ||
}); | ||
}); | ||
} | ||
}, | ||
'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({ | ||
|
@@ -85,6 +86,17 @@ ruleTester.run('no-broken-super-chain', rule, { | |
} | ||
], | ||
invalid: [ | ||
{ | ||
code: ` | ||
export default Ember.Service.extend({ | ||
init() { | ||
this.alias = this.concrete; | ||
} | ||
});`, | ||
errors: [{ | ||
message: `${noSuper} init` | ||
}] | ||
}, | ||
{ | ||
code: ` | ||
export default Ember.Component.extend({ | ||
|
@@ -93,7 +105,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 +149,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 +205,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/...