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