diff --git a/README.md b/README.md index 93913ed864..4f962296a7 100755 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ Finally, enable all of the rules that you would like to use. "meteor/no-session": 2, "meteor/no-blaze-lifecycle-assignment": 2, "meteor/no-zero-timeout": 2 + "meteor/blaze-consistent-eventmap-params": 2, } } ``` @@ -91,6 +92,7 @@ A complete example of how to set up ESLint-plugin-Meteor in a Meteor project can * [no-session](docs/rules/no-session.md): Prevent usage of Session * [no-blaze-lifecycle-assignment](docs/rules/no-blaze-lifecycle-assignment.md): Prevent deprecated template lifecycle callback assignments * [no-zero-timeout](docs/rules/no-zero-timeout.md): Prevent usage of Meteor.setTimeout with zero delay +* [blaze-consistent-eventmap-params](docs/rules/blaze-consistent-eventmap-params.md): Force consistent event handler parameters in event maps ## To Do diff --git a/docs/rules/blaze-consistent-eventmap-params.md b/docs/rules/blaze-consistent-eventmap-params.md new file mode 100644 index 0000000000..81edc9321f --- /dev/null +++ b/docs/rules/blaze-consistent-eventmap-params.md @@ -0,0 +1,83 @@ +# Consistent event handler parameters (blaze-consistent-eventmap-params) + +Force consistent event handler parameters in [event maps](http://docs.meteor.com/#/full/eventmaps) + + +## Rule Details + +Prevent the use of differently named parameters in event handlers to achieve more consistent code + +The following patterns are considered warnings: + +```js +// all on the client +Template.foo.events({ + // 'foo' does not match 'event' + 'submit form': function (foo) {} +}) + +Template.foo.events({ + // 'bar' does not match 'templateInstance' + 'submit form': function (event, bar) {} +}) + +Template.foo.events({ + // neither 'foo' nor 'bar' are correct + 'submit form': function (foo, bar) {} +}) + +``` + +The following patterns are not warnings: + +```js +// on the client +Template.foo.events({ + 'submit form': function (event) {} +}) + +// on the client +Template.foo.events({ + 'submit form': function (event, templateInstance) {} +}) + +``` + +### Options + +You can optionally set the names of the parameters. +You can set the name of the event parameter using `eventParamName` and the name of the template-instance parameterusing `templateInstanceParamName`. +Here are examples of how to do this: + +```js +/* + eslint meteor/blaze-consistent-eventmap-params: [2, {"eventParamName": "evt"}] + */ +Template.foo.events({ + 'submit form': function (evt) {} +}) + +/* + eslint meteor/blaze-consistent-eventmap-params: [2, {"templateInstanceParamName": "tmplInst"}] + */ +Template.foo.events({ + 'submit form': function (event, tmplInst) {} +}) + +/* + eslint meteor/blaze-consistent-eventmap-params: [2, {"eventParamName": "evt", "templateInstanceParamName": "tmplInst"}] + */ +Template.foo.events({ + 'submit form': function (evt, tmplInst) {} +}) + +``` + +## Limitations + +Checks client-side only. +If you use an event map in a universal file (server and client) then the `Meteor.isClient` checks must happen in `if`-conditions with exactly one condition. + +## Further Reading + +* http://docs.meteor.com/#/full/eventmaps diff --git a/lib/index.js b/lib/index.js index f097ca1d9c..a8e399abc6 100755 --- a/lib/index.js +++ b/lib/index.js @@ -28,7 +28,8 @@ module.exports = { 'audit-argument-checks': unpack('./rules/audit-argument-checks'), 'no-session': unpack('./rules/no-session'), 'no-blaze-lifecycle-assignment': unpack('./rules/no-blaze-lifecycle-assignment'), - 'no-zero-timeout': unpack('./rules/no-zero-timeout') + 'no-zero-timeout': unpack('./rules/no-zero-timeout'), + 'blaze-consistent-eventmap-params': unpack('./rules/blaze-consistent-eventmap-params') }, rulesConfig: { @@ -46,6 +47,7 @@ module.exports = { 'audit-argument-checks': 0, 'no-session': 0, 'no-blaze-lifecycle-assignment': 0, - 'no-zero-timeout': 0 + 'no-zero-timeout': 0, + 'blaze-consistent-eventmap-params': 0 } } diff --git a/lib/rules/blaze-consistent-eventmap-params.js b/lib/rules/blaze-consistent-eventmap-params.js new file mode 100644 index 0000000000..339acb61ec --- /dev/null +++ b/lib/rules/blaze-consistent-eventmap-params.js @@ -0,0 +1,93 @@ +/** + * @fileoverview Ensures consistent parameter names in blaze event maps + * @author Philipp Sporrer, Dominik Ferber + * @copyright 2015 Philipp Sporrer. All rights reserved. + * See LICENSE file in root directory for full license. + */ + +import {isFunction, isTemplateProp} from '../util/ast' +import {getExecutors} from '../util' +import {NON_METEOR} from '../util/environment' + +// ----------------------------------------------------------------------------- +// Rule Definition +// ----------------------------------------------------------------------------- + +module.exports = getMeta => context => { + + const {env} = getMeta(context) + + // --------------------------------------------------------------------------- + // Helpers + // --------------------------------------------------------------------------- + + function ensureParamName (param, expectedParamName) { + if (param && param.name !== expectedParamName) { + context.report( + param, + `Invalid parameter name, use "${expectedParamName}" instead` + ) + } + } + + function validateEventDef (eventDefNode) { + + const eventHandler = eventDefNode.value + if (isFunction(eventHandler.type)) { + + ensureParamName( + eventHandler.params[0], + context.options[0] ? context.options[0].eventParamName : 'event' + ) + + ensureParamName( + eventHandler.params[1], + context.options[0] ? context.options[0].templateInstanceParamName : 'templateInstance' + ) + } + + } + + // --------------------------------------------------------------------------- + // Public + // --------------------------------------------------------------------------- + + if (env === NON_METEOR) { + return {} + } + + return { + + CallExpression: function (node) { + + if (node.arguments.length === 0 || !isTemplateProp(node.callee, 'events')) { + return + } + const executors = getExecutors(env, context.getAncestors()) + if (executors.has('browser') || executors.has('cordova')) { + const eventMap = node.arguments[0] + + if (eventMap.type === 'ObjectExpression') { + eventMap.properties.forEach((eventDef) => validateEventDef(eventDef)) + } + } + } + + } + +} + +module.exports.schema = [ + { + type: 'object', + properties: { + eventParamName: { + type: 'string' + }, + templateInstanceParamName: { + type: 'string' + } + }, + additionalProperties: false + } +] diff --git a/lib/util/ast/index.js b/lib/util/ast/index.js index fbbacf489a..3e682ea22a 100644 --- a/lib/util/ast/index.js +++ b/lib/util/ast/index.js @@ -1,5 +1,6 @@ export {default as isMeteorCall} from './isMeteorCall' export {default as isMeteorProp} from './isMeteorProp' +export {default as isTemplateProp} from './isTemplateProp' export {default as isFunction} from './isFunction' export {default as getPropertyName} from './getPropertyName' export {default as areRefsTrackable} from './areRefsTrackable' diff --git a/lib/util/ast/isTemplateProp.js b/lib/util/ast/isTemplateProp.js new file mode 100644 index 0000000000..1d6f538cd8 --- /dev/null +++ b/lib/util/ast/isTemplateProp.js @@ -0,0 +1,10 @@ +import getPropertyName from './getPropertyName' + +export default function (node, propName) { + return ( + node.type === 'MemberExpression' && + node.object.type === 'MemberExpression' && + node.object.object.type === 'Identifier' && node.object.object.name === 'Template' && + getPropertyName(node.property) === propName + ) +} diff --git a/tests/lib/rules/blaze-consistent-eventmap-params.js b/tests/lib/rules/blaze-consistent-eventmap-params.js new file mode 100644 index 0000000000..7490ee8d06 --- /dev/null +++ b/tests/lib/rules/blaze-consistent-eventmap-params.js @@ -0,0 +1,253 @@ +/** + * @fileoverview Ensures that the names of the arguments of event handlers are always the same + * @author Philipp Sporrer, Dominik Ferber + * @copyright 2015 Philipp Sporrer. All rights reserved. + * See LICENSE file in root directory for full license. + */ + +// ----------------------------------------------------------------------------- +// Requirements +// ----------------------------------------------------------------------------- + +import { + CLIENT, + UNIVERSAL, + NON_METEOR +} from '../../../dist/util/environment.js' +const rule = require('../../../dist/rules/blaze-consistent-eventmap-params') +const RuleTester = require('eslint').RuleTester + +// ----------------------------------------------------------------------------- +// Tests +// ----------------------------------------------------------------------------- + +const ruleTester = new RuleTester() +ruleTester.run('blaze-consistent-eventmap-params', rule(() => ({env: CLIENT})), { + + valid: [ + ` + Foo.bar.events({ + 'submit form': function (bar, baz) { + // no error, because not on Template + } + }) + `, + ` + Template.foo.events({ + 'submit form': function (event) {} + }) + `, + ` + Template['foo'].events({ + 'submit form': function (event) {} + }) + `, + ` + Template['foo']['events']({ + 'submit form': function (event) {} + }) + `, + ` + Template.foo['events']({ + 'submit form': function (event) {} + }) + `, + ` + Template.foo.events({ + 'submit form': {} + }) + `, + ` + Template.foo.events() + `, + ` + Template.foo.events(null) + `, + { + code: ` + Template.foo.events({ + 'submit form': function (evt) {} + }) + `, + options: [{ + eventParamName: 'evt' + }] + }, + { + code: ` + Template.foo.events({ + 'submit form': function (evt, tmplInst) {} + }) + `, + options: [{ + eventParamName: 'evt', + templateInstanceParamName: 'tmplInst' + }] + }, + ` + Template.foo.events({ + 'submit form': function (event, templateInstance) {} + }) + `, + { + code: ` + Template.foo.events({ + 'submit form': (event, templateInstance) => {} + }) + `, + parser: 'babel-eslint' + } + ], + + invalid: [ + { + code: ` + Template.foo.events({ + 'submit form': function (foo, bar) {} + }) + `, + errors: [ + {message: 'Invalid parameter name, use "event" instead', type: 'Identifier'}, + {message: 'Invalid parameter name, use "templateInstance" instead', type: 'Identifier'} + ] + }, + { + code: ` + Template['foo'].events({ + 'submit form': function (foo, bar) {} + }) + `, + errors: [ + {message: 'Invalid parameter name, use "event" instead', type: 'Identifier'}, + {message: 'Invalid parameter name, use "templateInstance" instead', type: 'Identifier'} + ] + }, + { + code: ` + Template['foo']['events']({ + 'submit form': function (foo, bar) {} + }) + `, + errors: [ + {message: 'Invalid parameter name, use "event" instead', type: 'Identifier'}, + {message: 'Invalid parameter name, use "templateInstance" instead', type: 'Identifier'} + ] + }, + { + code: ` + Template.foo['events']({ + 'submit form': function (foo, bar) {} + }) + `, + errors: [ + {message: 'Invalid parameter name, use "event" instead', type: 'Identifier'}, + {message: 'Invalid parameter name, use "templateInstance" instead', type: 'Identifier'} + ] + }, + { + code: ` + Template.foo.events({ + 'submit form': (foo, bar) => {} + }) + `, + errors: [ + {message: 'Invalid parameter name, use "event" instead', type: 'Identifier'}, + {message: 'Invalid parameter name, use "templateInstance" instead', type: 'Identifier'} + ], + parser: 'babel-eslint' + }, + { + code: ` + Template.foo.events({ + 'submit form': function (foo, templateInstance) {} + }) + `, + errors: [ + {message: 'Invalid parameter name, use "event" instead', type: 'Identifier'} + ] + }, + { + code: ` + Template.foo.events({ + 'submit form': function (event, bar) {} + }) + `, + errors: [ + {message: 'Invalid parameter name, use "templateInstance" instead', type: 'Identifier'} + ] + } + ] + +}) + +ruleTester.run('blaze-consistent-eventmap-params', rule(() => ({env: UNIVERSAL})), { + + valid: [ + ` + Nontemplate.foo.events({ + 'submit form': function (bar, baz) { + // no error, because not on Template + } + }) + `, + ` + if (Meteor.isCordova) { + Template.foo.events({ + 'submit form': function (event, templateInstance) {} + }) + } + `, + ` + if (Meteor.isServer) { + Template.foo.events({ + 'submit form': function (bar, baz) {} + }) + } + `, + ` + if (Meteor.isClient) { + Template.foo.events({ + 'submit form': function (event, templateInstance) {} + }) + } + `, + { + code: ` + if (Meteor.isClient) { + Template.foo.events({ + 'submit form': (event, templateInstance) => {} + }) + } + `, + parser: 'babel-eslint' + } + ], + + invalid: [ + { + code: ` + if (Meteor.isClient) { + Template.foo.events({ + 'submit form': function (foo, bar) {} + }) + } + `, + errors: [ + {message: 'Invalid parameter name, use "event" instead', type: 'Identifier'}, + {message: 'Invalid parameter name, use "templateInstance" instead', type: 'Identifier'} + ] + } + ] + +}) + +ruleTester.run('blaze-consistent-eventmap-params', rule(() => ({env: NON_METEOR})), { + valid: [ + ` + Template.foo.events({ + 'submit form': function (foo, bar) {} + }) + ` + ], + invalid: [] +})