feat(blaze-consistent-eventmap-params): Add rule

closes #12, #33
This commit is contained in:
Dominik Ferber
2015-10-28 21:21:08 +01:00
parent 7d428c4ca4
commit be73ffa031
7 changed files with 446 additions and 2 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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
}
]

View File

@@ -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'

View File

@@ -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
)
}

View File

@@ -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: []
})