1 | 'use strict';
|
2 | const getDocumentationUrl = require('./utils/get-documentation-url');
|
3 | const domEventsJson = require('./utils/dom-events.json');
|
4 |
|
5 | const message = 'Prefer `{{replacement}}` over `{{method}}`.{{extra}}';
|
6 | const 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 |
|
11 | const nestedEvents = Object.values(domEventsJson);
|
12 | const eventTypes = new Set(nestedEvents.reduce((accumulatorEvents, events) => accumulatorEvents.concat(events), []));
|
13 | const getEventMethodName = memberExpression => memberExpression.property.name;
|
14 | const getEventTypeName = eventMethodName => eventMethodName.slice('on'.length);
|
15 |
|
16 | const 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 |
|
24 | const 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 |
|
39 | const 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 |
|
51 | const 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 |
|
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 |
|
144 | const 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 |
|
160 | module.exports = {
|
161 | create,
|
162 | meta: {
|
163 | type: 'suggestion',
|
164 | docs: {
|
165 | url: getDocumentationUrl(__filename)
|
166 | },
|
167 | fixable: 'code',
|
168 | schema
|
169 | }
|
170 | };
|