@pobammer-ts/eslint-cease-nonsense-rules 1.2.3 → 1.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/build-metadata.json +5 -0
- package/dist/index.js +11637 -80
- package/dist/index.js.map +515 -1
- package/package.json +10 -10
- package/dist/configure-utilities.js +0 -136
- package/dist/configure-utilities.js.map +0 -1
- package/dist/rules/ban-instances.js +0 -123
- package/dist/rules/ban-instances.js.map +0 -1
- package/dist/rules/ban-react-fc.js +0 -42
- package/dist/rules/ban-react-fc.js.map +0 -1
- package/dist/rules/enforce-ianitor-check-type.js +0 -340
- package/dist/rules/enforce-ianitor-check-type.js.map +0 -1
- package/dist/rules/no-color3-constructor.js +0 -76
- package/dist/rules/no-color3-constructor.js.map +0 -1
- package/dist/rules/no-instance-methods-without-this.js +0 -122
- package/dist/rules/no-instance-methods-without-this.js.map +0 -1
- package/dist/rules/no-print.js +0 -25
- package/dist/rules/no-print.js.map +0 -1
- package/dist/rules/no-shorthand-names.js +0 -116
- package/dist/rules/no-shorthand-names.js.map +0 -1
- package/dist/rules/no-warn.js +0 -25
- package/dist/rules/no-warn.js.map +0 -1
- package/dist/rules/prefer-sequence-overloads.js +0 -109
- package/dist/rules/prefer-sequence-overloads.js.map +0 -1
- package/dist/rules/prefer-udim2-shorthand.js +0 -161
- package/dist/rules/prefer-udim2-shorthand.js.map +0 -1
- package/dist/rules/require-named-effect-functions.js +0 -350
- package/dist/rules/require-named-effect-functions.js.map +0 -1
- package/dist/rules/require-paired-calls.js +0 -861
- package/dist/rules/require-paired-calls.js.map +0 -1
- package/dist/rules/require-react-component-keys.js +0 -357
- package/dist/rules/require-react-component-keys.js.map +0 -1
- package/dist/rules/use-exhaustive-dependencies.js +0 -697
- package/dist/rules/use-exhaustive-dependencies.js.map +0 -1
- package/dist/rules/use-hook-at-top-level.js +0 -351
- package/dist/rules/use-hook-at-top-level.js.map +0 -1
|
@@ -1,861 +0,0 @@
|
|
|
1
|
-
import { AST_NODE_TYPES } from "@typescript-eslint/types";
|
|
2
|
-
import Type from "typebox";
|
|
3
|
-
import { Compile } from "typebox/compile";
|
|
4
|
-
const isStringArray = Compile(Type.Readonly(Type.Array(Type.String())));
|
|
5
|
-
const isPairConfiguration = Compile(Type.Readonly(Type.Object({
|
|
6
|
-
alternatives: Type.Optional(isStringArray),
|
|
7
|
-
closer: Type.Union([Type.String(), isStringArray]),
|
|
8
|
-
opener: Type.String(),
|
|
9
|
-
openerAlternatives: Type.Optional(isStringArray),
|
|
10
|
-
platform: Type.Optional(Type.Literal("roblox")),
|
|
11
|
-
requireSync: Type.Optional(Type.Boolean()),
|
|
12
|
-
yieldingFunctions: Type.Optional(isStringArray),
|
|
13
|
-
})));
|
|
14
|
-
const isRuleOptions = Compile(Type.Partial(Type.Readonly(Type.Object({
|
|
15
|
-
allowConditionalClosers: Type.Optional(Type.Boolean()),
|
|
16
|
-
allowMultipleOpeners: Type.Optional(Type.Boolean()),
|
|
17
|
-
maxNestingDepth: Type.Optional(Type.Number()),
|
|
18
|
-
pairs: Type.Readonly(Type.Array(isPairConfiguration)),
|
|
19
|
-
}))));
|
|
20
|
-
const LOOP_NODE_TYPES = new Set([
|
|
21
|
-
AST_NODE_TYPES.DoWhileStatement,
|
|
22
|
-
AST_NODE_TYPES.ForInStatement,
|
|
23
|
-
AST_NODE_TYPES.ForOfStatement,
|
|
24
|
-
AST_NODE_TYPES.ForStatement,
|
|
25
|
-
AST_NODE_TYPES.WhileStatement,
|
|
26
|
-
]);
|
|
27
|
-
export const DEFAULT_ROBLOX_YIELDING_FUNCTIONS = ["task.wait", "wait", "*.WaitForChild", "*.*Async"];
|
|
28
|
-
function getCallName(node) {
|
|
29
|
-
const { callee } = node;
|
|
30
|
-
if (callee.type === AST_NODE_TYPES.Identifier)
|
|
31
|
-
return callee.name;
|
|
32
|
-
if (callee.type === AST_NODE_TYPES.MemberExpression) {
|
|
33
|
-
const object = callee.object.type === AST_NODE_TYPES.Identifier ? callee.object.name : undefined;
|
|
34
|
-
const property = callee.property.type === AST_NODE_TYPES.Identifier ? callee.property.name : undefined;
|
|
35
|
-
if (object !== undefined && property !== undefined)
|
|
36
|
-
return `${object}.${property}`;
|
|
37
|
-
}
|
|
38
|
-
return undefined;
|
|
39
|
-
}
|
|
40
|
-
function getValidClosers(configuration) {
|
|
41
|
-
const result = new Array();
|
|
42
|
-
if (isStringArray.Check(configuration.closer))
|
|
43
|
-
result.push(...configuration.closer);
|
|
44
|
-
else if (typeof configuration.closer === "string")
|
|
45
|
-
result.push(configuration.closer);
|
|
46
|
-
if (configuration.alternatives)
|
|
47
|
-
for (const alternative of configuration.alternatives)
|
|
48
|
-
result.push(alternative);
|
|
49
|
-
return result;
|
|
50
|
-
}
|
|
51
|
-
function getAllOpeners(configuration) {
|
|
52
|
-
const openers = [configuration.opener];
|
|
53
|
-
if (configuration.openerAlternatives)
|
|
54
|
-
openers.push(...configuration.openerAlternatives);
|
|
55
|
-
return openers;
|
|
56
|
-
}
|
|
57
|
-
function formatOpenerList(openers) {
|
|
58
|
-
if (openers.length === 0)
|
|
59
|
-
return "configured opener";
|
|
60
|
-
if (openers.length === 1)
|
|
61
|
-
return openers[0] ?? "configured opener";
|
|
62
|
-
return openers.join("' or '");
|
|
63
|
-
}
|
|
64
|
-
function isLoopLikeStatement(node) {
|
|
65
|
-
if (!node)
|
|
66
|
-
return false;
|
|
67
|
-
return LOOP_NODE_TYPES.has(node.type);
|
|
68
|
-
}
|
|
69
|
-
function isSwitchStatement(node) {
|
|
70
|
-
return node?.type === AST_NODE_TYPES.SwitchStatement;
|
|
71
|
-
}
|
|
72
|
-
function findLabeledStatementBody(label, startingNode) {
|
|
73
|
-
let current = startingNode;
|
|
74
|
-
while (current) {
|
|
75
|
-
if (current.type === AST_NODE_TYPES.LabeledStatement && current.label.name === label.name)
|
|
76
|
-
return current.body;
|
|
77
|
-
current = current.parent ?? undefined;
|
|
78
|
-
}
|
|
79
|
-
return undefined;
|
|
80
|
-
}
|
|
81
|
-
function resolveBreakTargetLoop(statement) {
|
|
82
|
-
const labeledBody = statement.label
|
|
83
|
-
? findLabeledStatementBody(statement.label, statement.parent ?? undefined)
|
|
84
|
-
: undefined;
|
|
85
|
-
if (labeledBody)
|
|
86
|
-
return isLoopLikeStatement(labeledBody) ? labeledBody : undefined;
|
|
87
|
-
let current = statement.parent ?? undefined;
|
|
88
|
-
while (current) {
|
|
89
|
-
if (isLoopLikeStatement(current))
|
|
90
|
-
return current;
|
|
91
|
-
if (isSwitchStatement(current))
|
|
92
|
-
return undefined;
|
|
93
|
-
current = current.parent ?? undefined;
|
|
94
|
-
}
|
|
95
|
-
return undefined;
|
|
96
|
-
}
|
|
97
|
-
function resolveContinueTargetLoop(statement) {
|
|
98
|
-
const labeledBody = statement.label
|
|
99
|
-
? findLabeledStatementBody(statement.label, statement.parent ?? undefined)
|
|
100
|
-
: undefined;
|
|
101
|
-
if (labeledBody)
|
|
102
|
-
return isLoopLikeStatement(labeledBody) ? labeledBody : undefined;
|
|
103
|
-
let current = statement.parent ?? undefined;
|
|
104
|
-
while (current) {
|
|
105
|
-
if (isLoopLikeStatement(current))
|
|
106
|
-
return current;
|
|
107
|
-
current = current.parent ?? undefined;
|
|
108
|
-
}
|
|
109
|
-
return undefined;
|
|
110
|
-
}
|
|
111
|
-
function cloneEntry(value) {
|
|
112
|
-
return { ...value, loopAncestors: [...value.loopAncestors] };
|
|
113
|
-
}
|
|
114
|
-
const rule = {
|
|
115
|
-
create(context) {
|
|
116
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- ESLint context.options is typed as any[]
|
|
117
|
-
const rawOptions = context.options[0];
|
|
118
|
-
const baseOptions = isRuleOptions.Check(rawOptions) ? rawOptions : {};
|
|
119
|
-
const options = {
|
|
120
|
-
allowConditionalClosers: baseOptions.allowConditionalClosers ?? false,
|
|
121
|
-
allowMultipleOpeners: baseOptions.allowMultipleOpeners ?? true,
|
|
122
|
-
maxNestingDepth: baseOptions.maxNestingDepth ?? 0,
|
|
123
|
-
pairs: baseOptions.pairs ?? [],
|
|
124
|
-
};
|
|
125
|
-
if (options.pairs.length === 0) {
|
|
126
|
-
options.pairs = [
|
|
127
|
-
{
|
|
128
|
-
closer: "debug.profileend",
|
|
129
|
-
opener: "debug.profilebegin",
|
|
130
|
-
platform: "roblox",
|
|
131
|
-
requireSync: true,
|
|
132
|
-
yieldingFunctions: [...DEFAULT_ROBLOX_YIELDING_FUNCTIONS],
|
|
133
|
-
},
|
|
134
|
-
];
|
|
135
|
-
}
|
|
136
|
-
const openerStack = new Array();
|
|
137
|
-
const loopStack = new Array();
|
|
138
|
-
let stackIndexCounter = 0;
|
|
139
|
-
const functionStacks = new Array();
|
|
140
|
-
let yieldingAutoClosed = false;
|
|
141
|
-
let yieldingReportedFirst = false;
|
|
142
|
-
const contextStack = new Array();
|
|
143
|
-
const stackSnapshots = new Map();
|
|
144
|
-
const branchStacks = new Map();
|
|
145
|
-
const closerToOpenersCache = new Map();
|
|
146
|
-
const openerToClosersCache = new Map();
|
|
147
|
-
function getConfiguredOpenersForCloser(closer) {
|
|
148
|
-
if (closerToOpenersCache.has(closer))
|
|
149
|
-
return closerToOpenersCache.get(closer) ?? [];
|
|
150
|
-
const names = new Array();
|
|
151
|
-
for (const pair of options.pairs) {
|
|
152
|
-
if (!getValidClosers(pair).includes(closer))
|
|
153
|
-
continue;
|
|
154
|
-
for (const openerName of getAllOpeners(pair))
|
|
155
|
-
if (!names.includes(openerName))
|
|
156
|
-
names.push(openerName);
|
|
157
|
-
}
|
|
158
|
-
closerToOpenersCache.set(closer, names);
|
|
159
|
-
return names;
|
|
160
|
-
}
|
|
161
|
-
function getExpectedClosersForOpener(opener) {
|
|
162
|
-
if (openerToClosersCache.has(opener))
|
|
163
|
-
return openerToClosersCache.get(opener) ?? [];
|
|
164
|
-
const closers = new Array();
|
|
165
|
-
for (const pair of options.pairs) {
|
|
166
|
-
const allOpeners = getAllOpeners(pair);
|
|
167
|
-
if (!allOpeners.includes(opener))
|
|
168
|
-
continue;
|
|
169
|
-
const validClosers = getValidClosers(pair);
|
|
170
|
-
for (const closer of validClosers)
|
|
171
|
-
if (!closers.includes(closer))
|
|
172
|
-
closers.push(closer);
|
|
173
|
-
}
|
|
174
|
-
openerToClosersCache.set(opener, closers);
|
|
175
|
-
return closers;
|
|
176
|
-
}
|
|
177
|
-
function getCurrentContext() {
|
|
178
|
-
return contextStack.length > 0
|
|
179
|
-
? // oxlint-disable-next-line no-non-null-assertion -- this is fine! we checked already!
|
|
180
|
-
contextStack.at(-1)
|
|
181
|
-
: {
|
|
182
|
-
asyncContext: false,
|
|
183
|
-
currentFunction: undefined,
|
|
184
|
-
hasEarlyExit: false,
|
|
185
|
-
inCatch: false,
|
|
186
|
-
inConditional: false,
|
|
187
|
-
inFinally: false,
|
|
188
|
-
inLoop: false,
|
|
189
|
-
inTry: false,
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
function pushContext(newContext) {
|
|
193
|
-
const currentContext = getCurrentContext();
|
|
194
|
-
contextStack.push({ ...currentContext, ...newContext });
|
|
195
|
-
}
|
|
196
|
-
function popContext() {
|
|
197
|
-
contextStack.pop();
|
|
198
|
-
}
|
|
199
|
-
function updateContext(updates) {
|
|
200
|
-
const last = contextStack.at(-1);
|
|
201
|
-
if (!last)
|
|
202
|
-
return;
|
|
203
|
-
contextStack[contextStack.length - 1] = { ...last, ...updates };
|
|
204
|
-
}
|
|
205
|
-
function cloneStack() {
|
|
206
|
-
// oxlint-disable-next-line no-array-callback-reference -- this is fine. leave it alone.
|
|
207
|
-
return openerStack.map(cloneEntry);
|
|
208
|
-
}
|
|
209
|
-
function saveSnapshot(node) {
|
|
210
|
-
stackSnapshots.set(node, cloneStack());
|
|
211
|
-
}
|
|
212
|
-
function findPairConfig(functionName, isOpener) {
|
|
213
|
-
return options.pairs.find((pair) => {
|
|
214
|
-
if (isOpener)
|
|
215
|
-
return getAllOpeners(pair).includes(functionName);
|
|
216
|
-
// Check if it matches closer or alternatives
|
|
217
|
-
const validClosers = getValidClosers(pair);
|
|
218
|
-
return validClosers.includes(functionName);
|
|
219
|
-
});
|
|
220
|
-
}
|
|
221
|
-
function isRobloxYieldingFunction(functionName, configuration) {
|
|
222
|
-
if (configuration.platform !== "roblox")
|
|
223
|
-
return false;
|
|
224
|
-
const yieldingFunctions = configuration.yieldingFunctions ?? DEFAULT_ROBLOX_YIELDING_FUNCTIONS;
|
|
225
|
-
return yieldingFunctions.some((pattern) => {
|
|
226
|
-
if (pattern.startsWith("*.")) {
|
|
227
|
-
// Match any method call with this name
|
|
228
|
-
const methodName = pattern.slice(2);
|
|
229
|
-
return functionName.endsWith(`.${methodName}`);
|
|
230
|
-
}
|
|
231
|
-
return functionName === pattern;
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
|
-
function onFunctionEnter(node) {
|
|
235
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- ESLint visitor functions receive unknown, we know the type from selector
|
|
236
|
-
const functionNode = node;
|
|
237
|
-
functionStacks.push([...openerStack]);
|
|
238
|
-
openerStack.length = 0;
|
|
239
|
-
yieldingAutoClosed = false;
|
|
240
|
-
yieldingReportedFirst = false;
|
|
241
|
-
pushContext({
|
|
242
|
-
asyncContext: functionNode.async ?? false,
|
|
243
|
-
currentFunction: functionNode,
|
|
244
|
-
hasEarlyExit: false,
|
|
245
|
-
inCatch: false,
|
|
246
|
-
inConditional: false,
|
|
247
|
-
inFinally: false,
|
|
248
|
-
inLoop: false,
|
|
249
|
-
inTry: false,
|
|
250
|
-
});
|
|
251
|
-
}
|
|
252
|
-
function onFunctionExit() {
|
|
253
|
-
if (openerStack.length > 0) {
|
|
254
|
-
for (const entry of openerStack) {
|
|
255
|
-
const validClosers = getValidClosers(entry.config);
|
|
256
|
-
const closer = validClosers.length === 1 ? (validClosers[0] ?? "closer") : validClosers.join("' or '");
|
|
257
|
-
context.report({
|
|
258
|
-
data: {
|
|
259
|
-
closer,
|
|
260
|
-
opener: entry.opener,
|
|
261
|
-
paths: "function exit",
|
|
262
|
-
},
|
|
263
|
-
messageId: "unpairedOpener",
|
|
264
|
-
node: entry.node,
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
const parentStack = functionStacks.pop();
|
|
269
|
-
if (parentStack) {
|
|
270
|
-
openerStack.length = 0;
|
|
271
|
-
openerStack.push(...parentStack);
|
|
272
|
-
}
|
|
273
|
-
else
|
|
274
|
-
openerStack.length = 0;
|
|
275
|
-
popContext();
|
|
276
|
-
}
|
|
277
|
-
function onIfStatementEnter(node) {
|
|
278
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- ESLint visitor type from selector
|
|
279
|
-
const ifNode = node;
|
|
280
|
-
pushContext({ inConditional: true });
|
|
281
|
-
saveSnapshot(ifNode);
|
|
282
|
-
}
|
|
283
|
-
function onIfStatementExit(node) {
|
|
284
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- ESLint visitor type from selector
|
|
285
|
-
const ifNode = node;
|
|
286
|
-
popContext();
|
|
287
|
-
const originalStack = stackSnapshots.get(ifNode);
|
|
288
|
-
const branches = branchStacks.get(ifNode);
|
|
289
|
-
if (originalStack && branches && branches.length > 0) {
|
|
290
|
-
const hasCompleteElse = ifNode.alternate !== undefined && ifNode.alternate !== null;
|
|
291
|
-
// Check for openers added in branches that weren't closed
|
|
292
|
-
for (const branchStack of branches) {
|
|
293
|
-
for (const entry of branchStack) {
|
|
294
|
-
const wasInOriginal = originalStack.some((o) => o.index === entry.index);
|
|
295
|
-
if (!wasInOriginal) {
|
|
296
|
-
// This opener was added in a branch and not closed
|
|
297
|
-
const validClosers = getValidClosers(entry.config);
|
|
298
|
-
const closer = validClosers.length === 1 ? (validClosers[0] ?? "closer") : validClosers.join("' or '");
|
|
299
|
-
context.report({
|
|
300
|
-
data: {
|
|
301
|
-
closer,
|
|
302
|
-
opener: entry.opener,
|
|
303
|
-
paths: "conditional branch",
|
|
304
|
-
},
|
|
305
|
-
messageId: "unpairedOpener",
|
|
306
|
-
node: entry.node,
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
if (hasCompleteElse) {
|
|
312
|
-
for (const { index, config, opener, node } of originalStack) {
|
|
313
|
-
const branchesWithOpener = branches.filter((branchStack) => branchStack.some((branch) => branch.index === index));
|
|
314
|
-
if (branchesWithOpener.length <= 0 || branchesWithOpener.length >= branches.length)
|
|
315
|
-
continue;
|
|
316
|
-
if (options.allowConditionalClosers !== false)
|
|
317
|
-
continue;
|
|
318
|
-
const validClosers = getValidClosers(config);
|
|
319
|
-
const closer = validClosers.length === 1 ? (validClosers[0] ?? "closer") : validClosers.join("' or '");
|
|
320
|
-
context.report({
|
|
321
|
-
data: {
|
|
322
|
-
closer,
|
|
323
|
-
opener,
|
|
324
|
-
paths: "not all execution paths",
|
|
325
|
-
},
|
|
326
|
-
messageId: "unpairedOpener",
|
|
327
|
-
node,
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
const commonOpeners = originalStack.filter((opener) => branches.every((branchStack) => branchStack.some(({ index }) => index === opener.index)));
|
|
331
|
-
openerStack.length = 0;
|
|
332
|
-
openerStack.push(...commonOpeners);
|
|
333
|
-
}
|
|
334
|
-
else {
|
|
335
|
-
openerStack.length = 0;
|
|
336
|
-
for (const entry of originalStack)
|
|
337
|
-
openerStack.push({ ...entry });
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
stackSnapshots.delete(ifNode);
|
|
341
|
-
branchStacks.delete(ifNode);
|
|
342
|
-
}
|
|
343
|
-
function onIfConsequentExit(node) {
|
|
344
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- ESLint visitor type from selector
|
|
345
|
-
const consequentNode = node;
|
|
346
|
-
const parent = consequentNode.parent;
|
|
347
|
-
if (parent?.type === AST_NODE_TYPES.IfStatement) {
|
|
348
|
-
const branches = branchStacks.get(parent) ?? [];
|
|
349
|
-
branches.push(cloneStack());
|
|
350
|
-
branchStacks.set(parent, branches);
|
|
351
|
-
const originalStack = stackSnapshots.get(parent);
|
|
352
|
-
if (!originalStack)
|
|
353
|
-
return;
|
|
354
|
-
openerStack.length = 0;
|
|
355
|
-
for (const entry of originalStack)
|
|
356
|
-
openerStack.push({ ...entry });
|
|
357
|
-
}
|
|
358
|
-
}
|
|
359
|
-
function onIfAlternateExit(node) {
|
|
360
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- ESLint visitor type from selector
|
|
361
|
-
const alternateNode = node;
|
|
362
|
-
const parent = alternateNode.parent;
|
|
363
|
-
if (parent?.type === AST_NODE_TYPES.IfStatement) {
|
|
364
|
-
const branches = branchStacks.get(parent) ?? [];
|
|
365
|
-
branches.push(cloneStack());
|
|
366
|
-
branchStacks.set(parent, branches);
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
function onTryStatementEnter(node) {
|
|
370
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- ESLint visitor type from selector
|
|
371
|
-
const tryNode = node;
|
|
372
|
-
saveSnapshot(tryNode);
|
|
373
|
-
}
|
|
374
|
-
function onTryStatementExit(node) {
|
|
375
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- ESLint visitor type from selector
|
|
376
|
-
const tryNode = node;
|
|
377
|
-
const originalStack = stackSnapshots.get(tryNode);
|
|
378
|
-
const branches = branchStacks.get(tryNode);
|
|
379
|
-
if (tryNode.finalizer) {
|
|
380
|
-
stackSnapshots.delete(tryNode);
|
|
381
|
-
branchStacks.delete(tryNode);
|
|
382
|
-
return;
|
|
383
|
-
}
|
|
384
|
-
if (originalStack && branches && branches.length > 0) {
|
|
385
|
-
for (const opener of originalStack) {
|
|
386
|
-
const branchesWithOpener = branches.filter((branchStack) => branchStack.some((entry) => entry.index === opener.index));
|
|
387
|
-
if (branchesWithOpener.length > 0 &&
|
|
388
|
-
branchesWithOpener.length < branches.length &&
|
|
389
|
-
options.allowConditionalClosers === false) {
|
|
390
|
-
const validClosers = getValidClosers(opener.config);
|
|
391
|
-
const closer = validClosers.length === 1 ? (validClosers[0] ?? "closer") : validClosers.join("' or '");
|
|
392
|
-
context.report({
|
|
393
|
-
data: {
|
|
394
|
-
closer,
|
|
395
|
-
opener: opener.opener,
|
|
396
|
-
paths: "not all execution paths",
|
|
397
|
-
},
|
|
398
|
-
messageId: "unpairedOpener",
|
|
399
|
-
node: opener.node,
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
}
|
|
403
|
-
const commonOpeners = originalStack.filter((opener) => branches.every((branchStack) => branchStack.some((entry) => entry.index === opener.index)));
|
|
404
|
-
openerStack.length = 0;
|
|
405
|
-
openerStack.push(...commonOpeners);
|
|
406
|
-
}
|
|
407
|
-
stackSnapshots.delete(tryNode);
|
|
408
|
-
branchStacks.delete(tryNode);
|
|
409
|
-
}
|
|
410
|
-
function onTryBlockEnter() {
|
|
411
|
-
pushContext({ inTry: true });
|
|
412
|
-
}
|
|
413
|
-
function onTryBlockExit(node) {
|
|
414
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- ESLint visitor type from selector
|
|
415
|
-
const blockNode = node;
|
|
416
|
-
const { parent } = blockNode;
|
|
417
|
-
if (parent?.type === AST_NODE_TYPES.TryStatement) {
|
|
418
|
-
const branches = branchStacks.get(parent) ?? [];
|
|
419
|
-
branches.push(cloneStack());
|
|
420
|
-
branchStacks.set(parent, branches);
|
|
421
|
-
const originalStack = stackSnapshots.get(parent);
|
|
422
|
-
if (originalStack) {
|
|
423
|
-
openerStack.length = 0;
|
|
424
|
-
for (const entry of originalStack)
|
|
425
|
-
openerStack.push({ ...entry });
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
popContext();
|
|
429
|
-
}
|
|
430
|
-
function onCatchClauseEnter() {
|
|
431
|
-
pushContext({ inCatch: true });
|
|
432
|
-
}
|
|
433
|
-
function onCatchClauseExit(node) {
|
|
434
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- ESLint visitor type from selector
|
|
435
|
-
const catchNode = node;
|
|
436
|
-
const { parent } = catchNode;
|
|
437
|
-
if (parent?.type === AST_NODE_TYPES.TryStatement) {
|
|
438
|
-
const branches = branchStacks.get(parent) ?? [];
|
|
439
|
-
branches.push(cloneStack());
|
|
440
|
-
branchStacks.set(parent, branches);
|
|
441
|
-
const originalStack = stackSnapshots.get(parent);
|
|
442
|
-
if (originalStack) {
|
|
443
|
-
openerStack.length = 0;
|
|
444
|
-
for (const entry of originalStack)
|
|
445
|
-
openerStack.push({ ...entry });
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
popContext();
|
|
449
|
-
}
|
|
450
|
-
function onFinallyBlockEnter() {
|
|
451
|
-
pushContext({ inFinally: true });
|
|
452
|
-
}
|
|
453
|
-
function onFinallyBlockExit() {
|
|
454
|
-
popContext();
|
|
455
|
-
}
|
|
456
|
-
function onSwitchStatementEnter(node) {
|
|
457
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- ESLint visitor type from selector
|
|
458
|
-
const switchNode = node;
|
|
459
|
-
pushContext({ inConditional: true });
|
|
460
|
-
saveSnapshot(switchNode);
|
|
461
|
-
}
|
|
462
|
-
function onSwitchStatementExit(node) {
|
|
463
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- ESLint visitor type from selector
|
|
464
|
-
const switchNode = node;
|
|
465
|
-
popContext();
|
|
466
|
-
const originalStack = stackSnapshots.get(switchNode);
|
|
467
|
-
const branches = branchStacks.get(switchNode);
|
|
468
|
-
if (originalStack && branches && branches.length > 0) {
|
|
469
|
-
const hasDefault = switchNode.cases.some((caseNode) => caseNode.test === null);
|
|
470
|
-
if (hasDefault && branches.length === switchNode.cases.length) {
|
|
471
|
-
for (const opener of originalStack) {
|
|
472
|
-
const branchesWithOpener = branches.filter((branchStack) => branchStack.some((entry) => entry.index === opener.index));
|
|
473
|
-
if (branchesWithOpener.length > 0 &&
|
|
474
|
-
branchesWithOpener.length < branches.length &&
|
|
475
|
-
options.allowConditionalClosers === false) {
|
|
476
|
-
const validClosers = getValidClosers(opener.config);
|
|
477
|
-
const closer = validClosers.length === 1 ? (validClosers[0] ?? "closer") : validClosers.join("' or '");
|
|
478
|
-
context.report({
|
|
479
|
-
data: {
|
|
480
|
-
closer,
|
|
481
|
-
opener: opener.opener,
|
|
482
|
-
paths: "not all execution paths",
|
|
483
|
-
},
|
|
484
|
-
messageId: "unpairedOpener",
|
|
485
|
-
node: opener.node,
|
|
486
|
-
});
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
const commonOpeners = originalStack.filter((opener) => branches.every((branchStack) => branchStack.some((entry) => entry.index === opener.index)));
|
|
490
|
-
openerStack.length = 0;
|
|
491
|
-
openerStack.push(...commonOpeners);
|
|
492
|
-
}
|
|
493
|
-
else {
|
|
494
|
-
openerStack.length = 0;
|
|
495
|
-
for (const entry of originalStack)
|
|
496
|
-
openerStack.push({ ...entry });
|
|
497
|
-
}
|
|
498
|
-
}
|
|
499
|
-
stackSnapshots.delete(switchNode);
|
|
500
|
-
branchStacks.delete(switchNode);
|
|
501
|
-
}
|
|
502
|
-
function onSwitchCaseExit(node) {
|
|
503
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- ESLint visitor type from selector
|
|
504
|
-
const caseNode = node;
|
|
505
|
-
const { parent } = caseNode;
|
|
506
|
-
if (parent?.type === AST_NODE_TYPES.SwitchStatement) {
|
|
507
|
-
const branches = branchStacks.get(parent) ?? [];
|
|
508
|
-
branches.push(cloneStack());
|
|
509
|
-
branchStacks.set(parent, branches);
|
|
510
|
-
const originalStack = stackSnapshots.get(parent);
|
|
511
|
-
if (!originalStack)
|
|
512
|
-
return;
|
|
513
|
-
openerStack.length = 0;
|
|
514
|
-
for (const entry of originalStack)
|
|
515
|
-
openerStack.push({ ...entry });
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
function onLoopEnter(node) {
|
|
519
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- ESLint visitor type from selector
|
|
520
|
-
const loopNode = node;
|
|
521
|
-
loopStack.push(loopNode);
|
|
522
|
-
pushContext({ inLoop: true });
|
|
523
|
-
}
|
|
524
|
-
function onLoopExit() {
|
|
525
|
-
if (loopStack.length > 0)
|
|
526
|
-
loopStack.pop();
|
|
527
|
-
popContext();
|
|
528
|
-
}
|
|
529
|
-
function onEarlyExit(node) {
|
|
530
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- ESLint visitor type from selector
|
|
531
|
-
const statementNode = node;
|
|
532
|
-
updateContext({ hasEarlyExit: true });
|
|
533
|
-
const currentContext = getCurrentContext();
|
|
534
|
-
if (currentContext.inFinally || openerStack.length === 0)
|
|
535
|
-
return;
|
|
536
|
-
for (const { opener, config, node } of openerStack) {
|
|
537
|
-
const validClosers = getValidClosers(config);
|
|
538
|
-
const closer = validClosers.length === 1 ? (validClosers[0] ?? "closer") : validClosers.join("' or '");
|
|
539
|
-
const statementType = statementNode.type === AST_NODE_TYPES.ReturnStatement ? "return" : "throw";
|
|
540
|
-
const lineNumber = statementNode.loc?.start.line ?? 0;
|
|
541
|
-
context.report({
|
|
542
|
-
data: {
|
|
543
|
-
closer,
|
|
544
|
-
opener,
|
|
545
|
-
paths: `${statementType} at line ${lineNumber}`,
|
|
546
|
-
},
|
|
547
|
-
messageId: "unpairedOpener",
|
|
548
|
-
node: node,
|
|
549
|
-
});
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
function onBreakContinue(node) {
|
|
553
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- ESLint visitor type from selector
|
|
554
|
-
const statementNode = node;
|
|
555
|
-
if (openerStack.length === 0)
|
|
556
|
-
return;
|
|
557
|
-
const targetLoop = statementNode.type === AST_NODE_TYPES.ContinueStatement
|
|
558
|
-
? resolveContinueTargetLoop(statementNode)
|
|
559
|
-
: resolveBreakTargetLoop(statementNode);
|
|
560
|
-
if (!targetLoop)
|
|
561
|
-
return;
|
|
562
|
-
for (const { node: openerNode, config, opener, loopAncestors } of openerStack) {
|
|
563
|
-
if (!loopAncestors.some((loopNode) => loopNode === targetLoop))
|
|
564
|
-
continue;
|
|
565
|
-
const validClosers = getValidClosers(config);
|
|
566
|
-
const closer = validClosers.length === 1 ? (validClosers[0] ?? "closer") : validClosers.join("' or '");
|
|
567
|
-
const statementType = statementNode.type === AST_NODE_TYPES.BreakStatement ? "break" : "continue";
|
|
568
|
-
const lineNumber = statementNode.loc?.start.line ?? 0;
|
|
569
|
-
context.report({
|
|
570
|
-
data: {
|
|
571
|
-
closer,
|
|
572
|
-
opener,
|
|
573
|
-
paths: `${statementType} at line ${lineNumber}`,
|
|
574
|
-
},
|
|
575
|
-
messageId: "unpairedOpener",
|
|
576
|
-
node: openerNode,
|
|
577
|
-
});
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
function onCallExpression(node) {
|
|
581
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- ESLint visitor type from selector
|
|
582
|
-
const callNode = node;
|
|
583
|
-
const callName = getCallName(callNode);
|
|
584
|
-
if (callName === undefined || callName === "")
|
|
585
|
-
return;
|
|
586
|
-
const openerConfig = findPairConfig(callName, true);
|
|
587
|
-
if (openerConfig) {
|
|
588
|
-
handleOpener(callNode, callName, openerConfig);
|
|
589
|
-
return;
|
|
590
|
-
}
|
|
591
|
-
if (findPairConfig(callName, false)) {
|
|
592
|
-
handleCloser(callNode, callName);
|
|
593
|
-
return;
|
|
594
|
-
}
|
|
595
|
-
for (const entry of openerStack) {
|
|
596
|
-
if (!isRobloxYieldingFunction(callName, entry.config))
|
|
597
|
-
continue;
|
|
598
|
-
handleRobloxYield(callNode, callName, entry);
|
|
599
|
-
// Roblox auto-closes ALL profiles
|
|
600
|
-
openerStack.length = 0;
|
|
601
|
-
yieldingAutoClosed = true;
|
|
602
|
-
return;
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
function handleOpener(node, opener, config) {
|
|
606
|
-
const maxDepth = options.maxNestingDepth ?? 0;
|
|
607
|
-
if (maxDepth > 0 && openerStack.length >= maxDepth) {
|
|
608
|
-
context.report({
|
|
609
|
-
data: { max: String(maxDepth) },
|
|
610
|
-
messageId: "maxNestingExceeded",
|
|
611
|
-
node,
|
|
612
|
-
});
|
|
613
|
-
}
|
|
614
|
-
if (options.allowMultipleOpeners === false &&
|
|
615
|
-
openerStack.length > 0 &&
|
|
616
|
-
openerStack.at(-1)?.opener === opener) {
|
|
617
|
-
context.report({
|
|
618
|
-
data: { opener },
|
|
619
|
-
messageId: "multipleOpeners",
|
|
620
|
-
node,
|
|
621
|
-
});
|
|
622
|
-
}
|
|
623
|
-
const entry = {
|
|
624
|
-
config,
|
|
625
|
-
index: stackIndexCounter++,
|
|
626
|
-
location: node.loc,
|
|
627
|
-
loopAncestors: [...loopStack],
|
|
628
|
-
node,
|
|
629
|
-
opener,
|
|
630
|
-
};
|
|
631
|
-
openerStack.push(entry);
|
|
632
|
-
}
|
|
633
|
-
function handleCloser(node, closer) {
|
|
634
|
-
const matchingIndex = openerStack.findLastIndex((entry) => getValidClosers(entry.config).includes(closer));
|
|
635
|
-
if (matchingIndex === -1) {
|
|
636
|
-
if (yieldingAutoClosed && !yieldingReportedFirst) {
|
|
637
|
-
yieldingReportedFirst = true;
|
|
638
|
-
return;
|
|
639
|
-
}
|
|
640
|
-
// Contextual error messages based on stack state
|
|
641
|
-
if (openerStack.length === 0) {
|
|
642
|
-
// Stack is empty - no opener to close
|
|
643
|
-
context.report({
|
|
644
|
-
data: { closer },
|
|
645
|
-
messageId: "unpairedCloser",
|
|
646
|
-
node,
|
|
647
|
-
});
|
|
648
|
-
}
|
|
649
|
-
else {
|
|
650
|
-
// Stack has openers, but this closer doesn't match any
|
|
651
|
-
// Show what closer was expected for the top opener
|
|
652
|
-
const topEntry = openerStack.at(-1);
|
|
653
|
-
if (topEntry) {
|
|
654
|
-
const expectedClosers = getExpectedClosersForOpener(topEntry.opener);
|
|
655
|
-
const closerDescription = formatOpenerList(expectedClosers);
|
|
656
|
-
context.report({
|
|
657
|
-
data: {
|
|
658
|
-
closer,
|
|
659
|
-
expected: closerDescription,
|
|
660
|
-
},
|
|
661
|
-
messageId: "unexpectedCloser",
|
|
662
|
-
node,
|
|
663
|
-
});
|
|
664
|
-
}
|
|
665
|
-
else {
|
|
666
|
-
// Fallback to old behavior if somehow no top entry
|
|
667
|
-
const openerCandidates = getConfiguredOpenersForCloser(closer);
|
|
668
|
-
const openerDescription = formatOpenerList(openerCandidates);
|
|
669
|
-
context.report({
|
|
670
|
-
data: {
|
|
671
|
-
closer,
|
|
672
|
-
opener: openerDescription,
|
|
673
|
-
},
|
|
674
|
-
messageId: "unpairedCloser",
|
|
675
|
-
node,
|
|
676
|
-
});
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
return;
|
|
680
|
-
}
|
|
681
|
-
const matchingEntry = openerStack[matchingIndex];
|
|
682
|
-
if (!matchingEntry)
|
|
683
|
-
return;
|
|
684
|
-
if (matchingIndex !== openerStack.length - 1) {
|
|
685
|
-
const topEntry = openerStack.at(-1);
|
|
686
|
-
if (topEntry) {
|
|
687
|
-
context.report({
|
|
688
|
-
data: {
|
|
689
|
-
actual: topEntry.opener,
|
|
690
|
-
closer,
|
|
691
|
-
expected: matchingEntry.opener,
|
|
692
|
-
},
|
|
693
|
-
messageId: "wrongOrder",
|
|
694
|
-
node,
|
|
695
|
-
});
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
openerStack.splice(matchingIndex, 1);
|
|
699
|
-
}
|
|
700
|
-
function handleRobloxYield(node, yieldingFunction, openerEntry) {
|
|
701
|
-
const validClosers = getValidClosers(openerEntry.config);
|
|
702
|
-
const closer = validClosers.length === 1 ? (validClosers[0] ?? "closer") : validClosers.join("' or '");
|
|
703
|
-
context.report({
|
|
704
|
-
data: { closer, yieldingFunction },
|
|
705
|
-
messageId: "robloxYieldViolation",
|
|
706
|
-
node,
|
|
707
|
-
});
|
|
708
|
-
}
|
|
709
|
-
function onAsyncYield(node) {
|
|
710
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- ESLint visitor type from selector
|
|
711
|
-
const asyncNode = node;
|
|
712
|
-
for (const { opener, config } of openerStack) {
|
|
713
|
-
if (config.requireSync !== true)
|
|
714
|
-
continue;
|
|
715
|
-
const validClosers = getValidClosers(config);
|
|
716
|
-
const closer = validClosers.length === 1 ? (validClosers[0] ?? "closer") : validClosers.join("' or '");
|
|
717
|
-
const asyncType = asyncNode.type === AST_NODE_TYPES.AwaitExpression ? "await" : "yield";
|
|
718
|
-
context.report({
|
|
719
|
-
data: { asyncType, closer, opener },
|
|
720
|
-
messageId: "asyncViolation",
|
|
721
|
-
node: asyncNode,
|
|
722
|
-
});
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
return {
|
|
726
|
-
ArrowFunctionExpression: onFunctionEnter,
|
|
727
|
-
"ArrowFunctionExpression:exit": onFunctionExit,
|
|
728
|
-
AwaitExpression: onAsyncYield,
|
|
729
|
-
BreakStatement: onBreakContinue,
|
|
730
|
-
CallExpression: onCallExpression,
|
|
731
|
-
CatchClause: onCatchClauseEnter,
|
|
732
|
-
"CatchClause:exit": onCatchClauseExit,
|
|
733
|
-
ContinueStatement: onBreakContinue,
|
|
734
|
-
DoWhileStatement: onLoopEnter,
|
|
735
|
-
"DoWhileStatement:exit": onLoopExit,
|
|
736
|
-
ForInStatement: onLoopEnter,
|
|
737
|
-
"ForInStatement:exit": onLoopExit,
|
|
738
|
-
ForOfStatement: (node) => {
|
|
739
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion -- ESLint visitor type from selector
|
|
740
|
-
const forOfNode = node;
|
|
741
|
-
if (forOfNode.await)
|
|
742
|
-
onAsyncYield(forOfNode);
|
|
743
|
-
onLoopEnter(forOfNode);
|
|
744
|
-
},
|
|
745
|
-
"ForOfStatement:exit": onLoopExit,
|
|
746
|
-
ForStatement: onLoopEnter,
|
|
747
|
-
"ForStatement:exit": onLoopExit,
|
|
748
|
-
FunctionDeclaration: onFunctionEnter,
|
|
749
|
-
"FunctionDeclaration:exit": onFunctionExit,
|
|
750
|
-
FunctionExpression: onFunctionEnter,
|
|
751
|
-
"FunctionExpression:exit": onFunctionExit,
|
|
752
|
-
IfStatement: onIfStatementEnter,
|
|
753
|
-
"IfStatement > .alternate:exit": onIfAlternateExit,
|
|
754
|
-
"IfStatement > .consequent:exit": onIfConsequentExit,
|
|
755
|
-
"IfStatement:exit": onIfStatementExit,
|
|
756
|
-
ReturnStatement: onEarlyExit,
|
|
757
|
-
"SwitchCase:exit": onSwitchCaseExit,
|
|
758
|
-
SwitchStatement: onSwitchStatementEnter,
|
|
759
|
-
"SwitchStatement:exit": onSwitchStatementExit,
|
|
760
|
-
ThrowStatement: onEarlyExit,
|
|
761
|
-
TryStatement: onTryStatementEnter,
|
|
762
|
-
"TryStatement > .block": onTryBlockEnter,
|
|
763
|
-
"TryStatement > .block:exit": onTryBlockExit,
|
|
764
|
-
"TryStatement > .finalizer": onFinallyBlockEnter,
|
|
765
|
-
"TryStatement > .finalizer:exit": onFinallyBlockExit,
|
|
766
|
-
"TryStatement:exit": onTryStatementExit,
|
|
767
|
-
WhileStatement: onLoopEnter,
|
|
768
|
-
"WhileStatement:exit": onLoopExit,
|
|
769
|
-
YieldExpression: onAsyncYield,
|
|
770
|
-
};
|
|
771
|
-
},
|
|
772
|
-
meta: {
|
|
773
|
-
docs: {
|
|
774
|
-
description: "Enforces balanced opener/closer function calls across all execution paths",
|
|
775
|
-
recommended: false,
|
|
776
|
-
url: "https://github.com/howmanysmall/eslint-idiot-lint/tree/main/docs/rules/require-paired-calls.md",
|
|
777
|
-
},
|
|
778
|
-
fixable: "code",
|
|
779
|
-
messages: {
|
|
780
|
-
asyncViolation: "Cannot use {{asyncType}} between '{{opener}}' and '{{closer}}' (requireSync: true)",
|
|
781
|
-
conditionalOpener: "Conditional opener '{{opener}}' at {{location}} may not have matching closer on all paths",
|
|
782
|
-
maxNestingExceeded: "Maximum nesting depth of {{max}} exceeded for paired calls",
|
|
783
|
-
multipleOpeners: "Multiple consecutive calls to '{{opener}}' without matching closers (allowMultipleOpeners: false)",
|
|
784
|
-
robloxYieldViolation: "Yielding function '{{yieldingFunction}}' auto-closes all profiles - subsequent '{{closer}}' will error",
|
|
785
|
-
unexpectedCloser: "Unexpected call to '{{closer}}' - expected one of: {{expected}}",
|
|
786
|
-
unpairedCloser: "Unexpected call to '{{closer}}' - no matching opener on stack",
|
|
787
|
-
unpairedOpener: "Unpaired call to '{{opener}}' - missing '{{closer}}' on {{paths}}",
|
|
788
|
-
wrongOrder: "Closer '{{closer}}' called out of order - expected to close '{{expected}}' but '{{actual}}' is still open",
|
|
789
|
-
},
|
|
790
|
-
schema: [
|
|
791
|
-
{
|
|
792
|
-
additionalProperties: false,
|
|
793
|
-
properties: {
|
|
794
|
-
allowConditionalClosers: {
|
|
795
|
-
default: false,
|
|
796
|
-
type: "boolean",
|
|
797
|
-
},
|
|
798
|
-
allowMultipleOpeners: {
|
|
799
|
-
default: true,
|
|
800
|
-
type: "boolean",
|
|
801
|
-
},
|
|
802
|
-
maxNestingDepth: {
|
|
803
|
-
default: 0,
|
|
804
|
-
minimum: 0,
|
|
805
|
-
type: "number",
|
|
806
|
-
},
|
|
807
|
-
pairs: {
|
|
808
|
-
items: {
|
|
809
|
-
additionalProperties: false,
|
|
810
|
-
properties: {
|
|
811
|
-
alternatives: {
|
|
812
|
-
items: { minLength: 1, type: "string" },
|
|
813
|
-
type: "array",
|
|
814
|
-
},
|
|
815
|
-
closer: {
|
|
816
|
-
oneOf: [
|
|
817
|
-
{ minLength: 1, type: "string" },
|
|
818
|
-
{
|
|
819
|
-
items: { minLength: 1, type: "string" },
|
|
820
|
-
minItems: 1,
|
|
821
|
-
type: "array",
|
|
822
|
-
},
|
|
823
|
-
],
|
|
824
|
-
},
|
|
825
|
-
opener: {
|
|
826
|
-
minLength: 1,
|
|
827
|
-
type: "string",
|
|
828
|
-
},
|
|
829
|
-
openerAlternatives: {
|
|
830
|
-
items: { minLength: 1, type: "string" },
|
|
831
|
-
type: "array",
|
|
832
|
-
},
|
|
833
|
-
platform: {
|
|
834
|
-
enum: ["roblox"],
|
|
835
|
-
type: "string",
|
|
836
|
-
},
|
|
837
|
-
requireSync: {
|
|
838
|
-
default: false,
|
|
839
|
-
type: "boolean",
|
|
840
|
-
},
|
|
841
|
-
yieldingFunctions: {
|
|
842
|
-
items: { minLength: 1, type: "string" },
|
|
843
|
-
type: "array",
|
|
844
|
-
},
|
|
845
|
-
},
|
|
846
|
-
required: ["opener", "closer"],
|
|
847
|
-
type: "object",
|
|
848
|
-
},
|
|
849
|
-
minItems: 1,
|
|
850
|
-
type: "array",
|
|
851
|
-
},
|
|
852
|
-
},
|
|
853
|
-
required: ["pairs"],
|
|
854
|
-
type: "object",
|
|
855
|
-
},
|
|
856
|
-
],
|
|
857
|
-
type: "problem",
|
|
858
|
-
},
|
|
859
|
-
};
|
|
860
|
-
export default rule;
|
|
861
|
-
//# sourceMappingURL=require-paired-calls.js.map
|