Skip to content

Commit 7b640fe

Browse files
committed
initial version
1 parent d77fbf7 commit 7b640fe

File tree

3 files changed

+568
-0
lines changed

3 files changed

+568
-0
lines changed

lib/rules/no-shadow-native-events.js

Lines changed: 371 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,371 @@
1+
/**
2+
* @author Jonathan Carle
3+
* See LICENSE file in root directory for full license.
4+
*/
5+
'use strict'
6+
7+
const utils = require('../utils')
8+
const domEvents = require('../utils/dom-events.json')
9+
const { capitalize } = require('../utils/casing.js')
10+
const {
11+
findVariable,
12+
isOpeningBraceToken,
13+
isClosingBraceToken,
14+
isOpeningBracketToken
15+
} = require('@eslint-community/eslint-utils')
16+
/**
17+
* @typedef {import('../utils').ComponentEmit} ComponentEmit
18+
* @typedef {import('../utils').ComponentProp} ComponentProp
19+
* @typedef {import('../utils').VueObjectData} VueObjectData
20+
*/
21+
22+
/**
23+
* Get the name param node from the given CallExpression
24+
* @param {CallExpression} node CallExpression
25+
* @returns { NameWithLoc | null }
26+
*/
27+
function getNameParamNode(node) {
28+
const nameLiteralNode = node.arguments[0]
29+
if (nameLiteralNode && utils.isStringLiteral(nameLiteralNode)) {
30+
const name = utils.getStringLiteralValue(nameLiteralNode)
31+
if (name != null) {
32+
return { name, loc: nameLiteralNode.loc, range: nameLiteralNode.range }
33+
}
34+
}
35+
36+
// cannot check
37+
return null
38+
}
39+
40+
/**
41+
* Check if the given name matches defineEmitsNode variable name
42+
* @param {string} name
43+
* @param {CallExpression | undefined} defineEmitsNode
44+
* @returns {boolean}
45+
*/
46+
function isEmitVariableName(name, defineEmitsNode) {
47+
const node = defineEmitsNode?.parent
48+
49+
if (node?.type === 'VariableDeclarator' && node.id.type === 'Identifier') {
50+
return name === node.id.name
51+
}
52+
53+
return false
54+
}
55+
56+
/**
57+
* @type {import('eslint').Rule.RuleModule}
58+
*/
59+
module.exports = {
60+
meta: {
61+
type: 'problem',
62+
docs: {
63+
description:
64+
'disallow the use of event names that collide with native web event names',
65+
categories: undefined,
66+
url: 'https://eslint.vuejs.org/rules/no-shadow-native-events.html'
67+
},
68+
fixable: null,
69+
hasSuggestions: false,
70+
schema: [
71+
{
72+
type: 'object',
73+
properties: {
74+
allowProps: {
75+
type: 'boolean'
76+
}
77+
},
78+
additionalProperties: false
79+
}
80+
],
81+
messages: {
82+
violation:
83+
'Use a different emit name to avoid shadowing native events: {{ name }}.'
84+
}
85+
},
86+
/** @param {RuleContext} context */
87+
create(context) {
88+
const options = context.options[0] || {}
89+
const allowProps = !!options.allowProps
90+
/** @type {Map<ObjectExpression | Program, { contextReferenceIds: Set<Identifier>, emitReferenceIds: Set<Identifier> }>} */
91+
const setupContexts = new Map()
92+
93+
/**
94+
* @typedef {object} VueTemplateDefineData
95+
* @property {'export' | 'mark' | 'definition' | 'setup'} type
96+
* @property {ObjectExpression | Program} define
97+
* @property {CallExpression} [defineEmits]
98+
*/
99+
/** @type {VueTemplateDefineData | null} */
100+
let vueTemplateDefineData = null
101+
102+
// TODO: needed?
103+
const programNode = context.getSourceCode().ast
104+
if (utils.isScriptSetup(context)) {
105+
// init
106+
vueTemplateDefineData = {
107+
type: 'setup',
108+
define: programNode
109+
}
110+
}
111+
112+
/**
113+
* @param {NameWithLoc} nameWithLoc
114+
*/
115+
function verifyEmit(nameWithLoc) {
116+
const name = nameWithLoc.name.toLowerCase()
117+
if (!domEvents.includes(name)) {
118+
return
119+
}
120+
context.report({
121+
loc: nameWithLoc.loc,
122+
messageId: 'violation',
123+
data: {
124+
name
125+
}
126+
})
127+
}
128+
129+
const callVisitor = {
130+
/**
131+
* @param {CallExpression} node
132+
* @param {VueObjectData} [info]
133+
*/
134+
CallExpression(node, info) {
135+
const callee = utils.skipChainExpression(node.callee)
136+
const nameWithLoc = getNameParamNode(node)
137+
if (!nameWithLoc) {
138+
// cannot check
139+
return
140+
}
141+
const vueDefineNode = info ? info.node : programNode
142+
143+
let emit
144+
if (callee.type === 'MemberExpression') {
145+
const name = utils.getStaticPropertyName(callee)
146+
if (name === 'emit' || name === '$emit') {
147+
emit = { name, member: callee }
148+
}
149+
}
150+
151+
// verify setup context
152+
const setupContext = setupContexts.get(vueDefineNode)
153+
if (setupContext) {
154+
const { contextReferenceIds, emitReferenceIds } = setupContext
155+
if (callee.type === 'Identifier' && emitReferenceIds.has(callee)) {
156+
// verify setup(props,{emit}) {emit()}
157+
verifyEmit(nameWithLoc)
158+
} else if (emit && emit.name === 'emit') {
159+
const memObject = utils.skipChainExpression(emit.member.object)
160+
if (
161+
memObject.type === 'Identifier' &&
162+
contextReferenceIds.has(memObject)
163+
) {
164+
// verify setup(props,context) {context.emit()}
165+
verifyEmit(nameWithLoc)
166+
}
167+
}
168+
}
169+
170+
// verify $emit
171+
if (emit && emit.name === '$emit') {
172+
const memObject = utils.skipChainExpression(emit.member.object)
173+
if (utils.isThis(memObject, context)) {
174+
// verify this.$emit()
175+
verifyEmit(nameWithLoc)
176+
}
177+
}
178+
}
179+
}
180+
/**
181+
* @param {ComponentEmit[]} emits
182+
*/
183+
const verifyEmitDeclaration = (emits) => {
184+
for (const { node, emitName } of emits) {
185+
if (!node || !emitName || !domEvents.includes(emitName.toLowerCase())) {
186+
continue
187+
}
188+
189+
context.report({
190+
messageId: 'violation',
191+
data: { name: emitName },
192+
loc: node.loc
193+
})
194+
}
195+
}
196+
197+
return utils.compositingVisitors(
198+
utils.defineTemplateBodyVisitor(
199+
context,
200+
{
201+
/** @param { CallExpression } node */
202+
CallExpression(node) {
203+
const callee = utils.skipChainExpression(node.callee)
204+
const nameWithLoc = getNameParamNode(node)
205+
if (!nameWithLoc) {
206+
// cannot check
207+
return
208+
}
209+
210+
// e.g. $emit() / emit() in template
211+
if (
212+
callee.type === 'Identifier' &&
213+
(callee.name === '$emit' ||
214+
(vueTemplateDefineData?.defineEmits &&
215+
isEmitVariableName(
216+
callee.name,
217+
vueTemplateDefineData.defineEmits
218+
)))
219+
) {
220+
verifyEmit(nameWithLoc)
221+
}
222+
}
223+
},
224+
utils.compositingVisitors(
225+
utils.defineScriptSetupVisitor(context, {
226+
onDefineEmitsEnter: (node, emits) => {
227+
verifyEmitDeclaration(emits)
228+
229+
// TODO: needed?
230+
if (
231+
vueTemplateDefineData &&
232+
vueTemplateDefineData.type === 'setup'
233+
) {
234+
vueTemplateDefineData.defineEmits = node
235+
}
236+
237+
if (
238+
!node.parent ||
239+
node.parent.type !== 'VariableDeclarator' ||
240+
node.parent.init !== node
241+
) {
242+
return
243+
}
244+
245+
const emitParam = node.parent.id
246+
const variable =
247+
emitParam.type === 'Identifier'
248+
? findVariable(utils.getScope(context, emitParam), emitParam)
249+
: null
250+
if (!variable) {
251+
return
252+
}
253+
/** @type {Set<Identifier>} */
254+
const emitReferenceIds = new Set()
255+
for (const reference of variable.references) {
256+
if (!reference.isRead()) {
257+
continue
258+
}
259+
260+
emitReferenceIds.add(reference.identifier)
261+
}
262+
setupContexts.set(programNode, {
263+
contextReferenceIds: new Set(),
264+
emitReferenceIds
265+
})
266+
},
267+
onDefinePropsEnter: (node, props) => {
268+
if (allowProps) {
269+
// TODO: verify props
270+
}
271+
},
272+
...callVisitor
273+
}),
274+
utils.defineVueVisitor(context, {
275+
onSetupFunctionEnter(node, { node: vueNode }) {
276+
const contextParam = node.params[1]
277+
if (!contextParam) {
278+
// no arguments
279+
return
280+
}
281+
if (contextParam.type === 'RestElement') {
282+
// cannot check
283+
return
284+
}
285+
if (contextParam.type === 'ArrayPattern') {
286+
// cannot check
287+
return
288+
}
289+
/** @type {Set<Identifier>} */
290+
const contextReferenceIds = new Set()
291+
/** @type {Set<Identifier>} */
292+
const emitReferenceIds = new Set()
293+
if (contextParam.type === 'ObjectPattern') {
294+
const emitProperty = utils.findAssignmentProperty(
295+
contextParam,
296+
'emit'
297+
)
298+
if (!emitProperty) {
299+
return
300+
}
301+
const emitParam = emitProperty.value
302+
// `setup(props, {emit})`
303+
const variable =
304+
emitParam.type === 'Identifier'
305+
? findVariable(
306+
utils.getScope(context, emitParam),
307+
emitParam
308+
)
309+
: null
310+
if (!variable) {
311+
return
312+
}
313+
for (const reference of variable.references) {
314+
if (!reference.isRead()) {
315+
continue
316+
}
317+
318+
emitReferenceIds.add(reference.identifier)
319+
}
320+
} else if (contextParam.type === 'Identifier') {
321+
// `setup(props, context)`
322+
const variable = findVariable(
323+
utils.getScope(context, contextParam),
324+
contextParam
325+
)
326+
if (!variable) {
327+
return
328+
}
329+
for (const reference of variable.references) {
330+
if (!reference.isRead()) {
331+
continue
332+
}
333+
334+
contextReferenceIds.add(reference.identifier)
335+
}
336+
}
337+
setupContexts.set(vueNode, {
338+
contextReferenceIds,
339+
emitReferenceIds
340+
})
341+
},
342+
onVueObjectEnter(node) {
343+
const emits = utils.getComponentEmitsFromOptions(node)
344+
verifyEmitDeclaration(emits)
345+
346+
if (allowProps) {
347+
// TODO: verify props
348+
// utils.getComponentPropsFromOptions(node)
349+
}
350+
},
351+
onVueObjectExit(node, { type }) {
352+
if (
353+
(!vueTemplateDefineData ||
354+
(vueTemplateDefineData.type !== 'export' &&
355+
vueTemplateDefineData.type !== 'setup')) &&
356+
(type === 'mark' || type === 'export' || type === 'definition')
357+
) {
358+
vueTemplateDefineData = {
359+
type,
360+
define: node
361+
}
362+
}
363+
setupContexts.delete(node)
364+
},
365+
...callVisitor
366+
})
367+
)
368+
)
369+
)
370+
}
371+
}

0 commit comments

Comments
 (0)