UNPKG

4.25 kBJavaScriptView Raw
1'use strict';
2const getDocumentationUrl = require('./utils/get-documentation-url');
3const domEventsJson = require('./utils/dom-events.json');
4const {flatten} = require('lodash');
5
6const message = 'Prefer `{{replacement}}` over `{{method}}`.{{extra}}';
7const extraMessages = {
8 beforeunload: 'Use `event.preventDefault(); event.returnValue = \'foo\'` to trigger the prompt.',
9 message: 'Note that there is difference between `SharedWorker#onmessage` and `SharedWorker#addEventListener(\'message\')`.'
10};
11
12const nestedEvents = Object.values(domEventsJson);
13const eventTypes = new Set(flatten(nestedEvents));
14const getEventMethodName = memberExpression => memberExpression.property.name;
15const getEventTypeName = eventMethodName => eventMethodName.slice('on'.length);
16
17const fixCode = (fixer, sourceCode, assignmentNode, memberExpression) => {
18 const eventTypeName = getEventTypeName(getEventMethodName(memberExpression));
19 const eventObjectCode = sourceCode.getText(memberExpression.object);
20 const fncCode = sourceCode.getText(assignmentNode.right);
21 const fixedCodeStatement = `${eventObjectCode}.addEventListener('${eventTypeName}', ${fncCode})`;
22 return fixer.replaceText(assignmentNode, fixedCodeStatement);
23};
24
25const shouldFixBeforeUnload = (assignedExpression, nodeReturnsSomething) => {
26 if (
27 assignedExpression.type !== 'ArrowFunctionExpression' &&
28 assignedExpression.type !== 'FunctionExpression'
29 ) {
30 return false;
31 }
32
33 if (assignedExpression.body.type !== 'BlockStatement') {
34 return false;
35 }
36
37 return !nodeReturnsSomething.get(assignedExpression);
38};
39
40const isClearing = node => {
41 if (node.type === 'Literal') {
42 return node.raw === 'null';
43 }
44
45 if (node.type === 'Identifier') {
46 return node.name === 'undefined';
47 }
48
49 return false;
50};
51
52const create = context => {
53 const options = context.options[0] || {};
54 const excludedPackages = new Set(options.excludedPackages || ['koa', 'sax']);
55 let isDisabled;
56
57 const nodeReturnsSomething = new WeakMap();
58 let codePathInfo;
59
60 return {
61 onCodePathStart(codePath, node) {
62 codePathInfo = {
63 node,
64 upper: codePathInfo,
65 returnsSomething: false
66 };
67 },
68
69 onCodePathEnd() {
70 nodeReturnsSomething.set(codePathInfo.node, codePathInfo.returnsSomething);
71 codePathInfo = codePathInfo.upper;
72 },
73
74 'CallExpression[callee.name="require"] > Literal'(node) {
75 if (!isDisabled && excludedPackages.has(node.value)) {
76 isDisabled = true;
77 }
78 },
79
80 'ImportDeclaration > Literal'(node) {
81 if (!isDisabled && excludedPackages.has(node.value)) {
82 isDisabled = true;
83 }
84 },
85
86 ReturnStatement(node) {
87 codePathInfo.returnsSomething = codePathInfo.returnsSomething || Boolean(node.argument);
88 },
89
90 'AssignmentExpression:exit'(node) {
91 if (isDisabled) {
92 return;
93 }
94
95 const {left: memberExpression, right: assignedExpression} = node;
96
97 if (memberExpression.type !== 'MemberExpression') {
98 return;
99 }
100
101 const eventMethodName = getEventMethodName(memberExpression);
102
103 if (!eventMethodName || !eventMethodName.startsWith('on')) {
104 return;
105 }
106
107 const eventTypeName = getEventTypeName(eventMethodName);
108
109 if (!eventTypes.has(eventTypeName)) {
110 return;
111 }
112
113 let replacement = 'addEventListener';
114 let extra = '';
115 let fix;
116
117 if (isClearing(assignedExpression)) {
118 replacement = 'removeEventListener';
119 } else if (
120 eventTypeName === 'beforeunload' &&
121 !shouldFixBeforeUnload(assignedExpression, nodeReturnsSomething)
122 ) {
123 extra = extraMessages.beforeunload;
124 } else if (eventTypeName === 'message') {
125 // Disable `onmessage` fix, see #537
126 extra = extraMessages.message;
127 } else {
128 fix = fixer => fixCode(fixer, context.getSourceCode(), node, memberExpression);
129 }
130
131 context.report({
132 node,
133 message,
134 data: {
135 replacement,
136 method: eventMethodName,
137 extra: extra ? ` ${extra}` : ''
138 },
139 fix
140 });
141 }
142 };
143};
144
145const schema = [
146 {
147 type: 'object',
148 properties: {
149 excludedPackages: {
150 type: 'array',
151 items: {
152 type: 'string'
153 },
154 uniqueItems: true
155 }
156 },
157 additionalProperties: false
158 }
159];
160
161module.exports = {
162 create,
163 meta: {
164 type: 'suggestion',
165 docs: {
166 url: getDocumentationUrl(__filename)
167 },
168 fixable: 'code',
169 schema
170 }
171};