-
-
Notifications
You must be signed in to change notification settings - Fork 247
/
Copy patheslint.config.js
321 lines (315 loc) · 11.6 KB
/
eslint.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
const js = require("@eslint/js");
const globals = require("globals");
const esquery = require("esquery");
const reportQueryMatches = (context, ast, selector, message) => {
const parsedSelector = esquery.parse(selector);
const matches = esquery.match(ast, parsedSelector);
for (const match of matches) {
context.report({
node: match,
message,
});
}
};
/**
* Allows creating custom rules that work similar to ESLint's no-restricted-syntax.
* @param {Array<{selector: string; message: string;}>} rules
*/
const createQueryRule = (rules) => ({
create: (context) => ({
Program: (node) => {
for (const rule of rules) {
reportQueryMatches(context, node, rule.selector, rule.message);
}
},
}),
});
module.exports = [
// Base on eslint recommended
js.configs.recommended,
// Common for all files
{
languageOptions: {
ecmaVersion: 2022,
sourceType: "commonjs",
},
rules: {
// Unused variables commonly indicate logic errors
"no-unused-vars": [
"error",
{
// Unused arguments are useful, eg. it can be nice for blocks to accept `args` even if they don't use it
args: "none",
// Allow silently eating try { } catch (e) { }
caughtErrors: "none",
// Variables starting with _ are intentionally unused
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
},
],
// Allow while (true) { }
"no-constant-condition": [
"error",
{
checkLoops: false,
},
],
// Allow empty catch {} blocks
"no-empty": [
"error",
{
allowEmptyCatch: true,
},
],
// Returning a value from a constructor() implies a mistake
"no-constructor-return": "error",
// new Promise(async () => {}) implies a mistake
"no-async-promise-executor": "warn",
// x === x implies a mistake
"no-self-compare": "error",
// Using ${...} in a non-template-string implies a mistake
"no-template-curly-in-string": "error",
// Loops that only iterate once imply a mistake
"no-unreachable-loop": "error",
// Detect some untrusted code execution
"no-eval": "error",
"no-implied-eval": "error",
"no-new-func": "error",
"no-script-url": "error",
// Combinations of || and && are unreadable and may not do what you expect
"no-mixed-operators": [
"error",
{
groups: [["&&", "||"]],
},
],
// Disallow async functions that don't need to be. This is important as a Promise and non-Promise return value
// significantly impacts the behavior of projects.
"require-await": "error",
},
},
// For development server
{
files: ["development/**"],
languageOptions: {
globals: {
...globals.commonjs,
...globals.node,
},
},
},
// For extensions
{
files: ["extensions/**"],
languageOptions: {
globals: {
...globals.browser,
Blockly: "readonly",
Scratch: "readonly",
ScratchBlocks: "readonly",
ScratchExtensions: "readonly",
scaffolding: "readonly",
},
},
plugins: {
extension: {
rules: {
"no-new-syntax": createQueryRule([
{
selector: 'AssignmentExpression[operator="??="]',
message: "x ??= y syntax is too new; use x = x ?? y intead",
},
{
selector:
"MemberExpression[object.name=Object][property.name=hasOwn]",
message:
"Object.hasOwn(...) is too new; use Object.prototype.hasOwnProperty.call(...) instead",
},
]),
"no-xmlhttprequest": createQueryRule([
{
selector: "NewExpression[callee.name=XMLHttpRequest]",
message: "Use Scratch.fetch() instead of XMLHttpRequest",
},
]),
iife: createQueryRule([
{
selector:
"Program > :not(ExpressionStatement[expression.type=CallExpression][expression.callee.type=/FunctionExpression/])",
message:
"All extension code must be within (function (Scratch) { ... })(Scratch);",
},
]),
"use-scratch-vm": createQueryRule([
{
selector:
"MemberExpression[object.name=window][property.name=vm]",
message: "Use Scratch.vm instead of window.vm",
},
]),
"use-scratch-fetch": createQueryRule([
{
selector: "CallExpression[callee.name=fetch]",
message: "Use Scratch.fetch() instead of fetch()",
},
{
selector:
"CallExpression[callee.object.name=window][callee.property.name=fetch]",
message: "Use Scratch.fetch() instead of window.fetch()",
},
]),
"use-scratch-open-window": createQueryRule([
{
selector: "CallExpression[callee.name=open]",
message: "Use Scratch.openWindow() instead of open()",
},
{
selector:
"CallExpression[callee.object.name=window][callee.property.name=open]",
message: "Use Scratch.openWindow() instead of window.open()",
},
]),
"use-scratch-redirect": createQueryRule([
{
selector:
"AssignmentExpression[left.object.name=location][left.property.name=href]",
message: "Use Scratch.redirect() instead of location.href = ...",
},
{
selector:
"AssignmentExpression[left.object.object.name=window][left.object.property.name=location][left.property.name=href]",
message:
"Use Scratch.redirect() instead of window.location.href = ...",
},
{
selector: "AssignmentExpression[left.name=location]",
message: "Use Scratch.redirect() instead of location = ...",
},
{
selector:
"AssignmentExpression[left.object.name=window][left.property.name=location]",
message:
"Use Scratch.redirect() instead of window.location = ...",
},
{
selector:
"CallExpression[callee.object.name=location][callee.property.name=assign]",
message: "Use Scratch.redirect() instead of location.assign()",
},
{
selector:
"CallExpression[callee.object.object.name=window][callee.object.property.name=location][callee.property.name=assign]",
message:
"Use Scratch.redirect() instead of window.location.assign()",
},
{
selector:
"CallExpression[callee.object.name=location][callee.property.name=replace]",
message: "Use Scratch.redirect() instead of location.replace()",
},
{
selector:
"CallExpression[callee.object.object.name=window][callee.object.property.name=location][callee.property.name=replace]",
message:
"Use Scratch.redirect() instead of window.location.replace()",
},
]),
"check-can-fetch": createQueryRule([
{
selector: "NewExpression[callee.name=WebSocket]",
message:
"Ensure that `await Scratch.canFetch(url)` is checked first, then add // eslint-disable-next-line extension/check-can-fetch",
},
{
selector: "NewExpression[callee.name=Image]",
message:
"Ensure that `await Scratch.canFetch(url)` is checked first, then add // eslint-disable-next-line extension/check-can-fetch",
},
{
selector: "NewExpression[callee.name=Audio]",
message:
"Ensure that `await Scratch.canFetch(url)` is checked first, then add // eslint-disable-next-line extension/check-can-fetch",
},
]),
"no-translate-setup": createQueryRule([
{
selector:
"CallExpression[callee.object.object.name=Scratch][callee.object.property.name=translate][callee.property.name=setup]",
message:
"Do not call Scratch.translate.setup() yourself. Just use Scratch.translate() and let the build script handle it",
},
]),
"no-translate-alias": createQueryRule([
{
selector:
"VariableDeclarator[init.type=MemberExpression][init.object.name=Scratch][init.property.name=translate]",
message:
"Do not store Scratch.translate in a variable as the build script will not be able to statically analyze the strings",
},
{
selector:
"AssignmentExpression[right.type=MemberExpression][right.object.name=Scratch][right.property.name=translate]",
message:
"Do not store Scratch.translate in a variable as the build script will not be able to statically analyze the strings",
},
]),
"should-translate": createQueryRule([
{
selector:
"MethodDefinition[key.name=getInfo] Property[key.name=name][value.type=Literal]",
message: "Extension name should usually be translated",
},
{
selector:
"MethodDefinition[key.name=getInfo] Property[key.name=blocks] Property[key.name=text][value.type=Literal]",
message: "Block text should usually be translated",
},
]),
"should-not-translate": createQueryRule([
{
selector:
"MethodDefinition[key.name=getInfo] Property[key.name=id][value.callee.property.name=translate]",
message: "Do not translate extension ID",
},
{
selector:
"MethodDefinition[key.name=docsURI] Property[key.name=id][value.callee.property.name=translate]",
message: "Do not translate docsURI",
},
{
selector:
"MethodDefinition[key.name=getInfo] Property[key.name=opcode][value.callee.property.name=translate]",
message: "Do not translate block opcode",
},
]),
},
},
},
rules: {
// Require each extension to use strict mode
strict: ["error", "function"],
// Disallow APIs that are replaced by Scratch.* APIs
// This is not comprehensive, but it should be enough to prevent the most common ways for these to be written.
"no-restricted-globals": [
"error",
{
name: "vm",
message:
"Use Scratch.vm instead of the global vm object. You also can use const vm = Scratch.vm;",
},
],
"extension/no-new-syntax": "error",
"extension/no-xmlhttprequest": "error",
"extension/iife": "error",
"extension/use-scratch-vm": "error",
"extension/use-scratch-fetch": "error",
"extension/use-scratch-open-window": "error",
"extension/use-scratch-redirect": "error",
"extension/check-can-fetch": "error",
"extension/no-translate-setup": "error",
"extension/no-translate-alias": "error",
"extension/should-translate": "error",
"extension/should-not-translate": "error",
},
},
];