UNPKG

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