eslint-plugin-discordjs 0.0.1
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/LICENSE +190 -0
- package/README.md +12 -0
- package/dist/index.cjs +861 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.mts +8 -0
- package/dist/index.mjs +859 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +63 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,861 @@
|
|
|
1
|
+
Object.defineProperties(exports, { __esModule: { value: true }, [Symbol.toStringTag]: { value: 'Module' } });
|
|
2
|
+
let _typescript_eslint_utils = require("@typescript-eslint/utils");
|
|
3
|
+
let typescript = require("typescript");
|
|
4
|
+
|
|
5
|
+
//#region src/createRule.ts
|
|
6
|
+
const createRule = _typescript_eslint_utils.ESLintUtils.RuleCreator((name) => `https://github.com/seedcord/seedcord/blob/next/packages/eslint-plugin-discordjs/docs/rules/${name}.md`);
|
|
7
|
+
|
|
8
|
+
//#endregion
|
|
9
|
+
//#region ../eslint-utils/dist/index.mjs
|
|
10
|
+
function isFromDiscordJs(symbol) {
|
|
11
|
+
const file = symbol?.declarations?.[0]?.getSourceFile().fileName;
|
|
12
|
+
return file !== void 0 && (file.includes("/discord.js/") || file.includes("/@discordjs/"));
|
|
13
|
+
}
|
|
14
|
+
function asClassOrInterface(type) {
|
|
15
|
+
if (type.isClassOrInterface()) return type;
|
|
16
|
+
const target = type.target;
|
|
17
|
+
if (target !== void 0 && target !== type && target.isClassOrInterface()) return target;
|
|
18
|
+
}
|
|
19
|
+
function walkBaseChain(checker, type, match) {
|
|
20
|
+
const seen = /* @__PURE__ */ new Set();
|
|
21
|
+
const stack = [type];
|
|
22
|
+
while (stack.length > 0) {
|
|
23
|
+
const current = stack.pop();
|
|
24
|
+
if (current === void 0 || seen.has(current)) continue;
|
|
25
|
+
seen.add(current);
|
|
26
|
+
if (current.isUnionOrIntersection()) {
|
|
27
|
+
stack.push(...current.types);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
const symbol = current.getSymbol();
|
|
31
|
+
if (symbol !== void 0 && match(symbol)) return true;
|
|
32
|
+
const iface = asClassOrInterface(current);
|
|
33
|
+
if (iface !== void 0) stack.push(...checker.getBaseTypes(iface));
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
function extendsDjsType(checker, type, names) {
|
|
38
|
+
const wanted = typeof names === "string" ? new Set([names]) : names;
|
|
39
|
+
return walkBaseChain(checker, type, (symbol) => wanted.has(symbol.getName()) && isFromDiscordJs(symbol));
|
|
40
|
+
}
|
|
41
|
+
function booleanLiteralValue(checker, type) {
|
|
42
|
+
if ((type.flags & typescript.TypeFlags.BooleanLiteral) === 0) return void 0;
|
|
43
|
+
return checker.typeToString(type) === "true";
|
|
44
|
+
}
|
|
45
|
+
function extendsSeedcordType(checker, type, names) {
|
|
46
|
+
const wanted = typeof names === "string" ? new Set([names]) : names;
|
|
47
|
+
return walkBaseChain(checker, type, (symbol) => wanted.has(symbol.getName()));
|
|
48
|
+
}
|
|
49
|
+
function methodName(call) {
|
|
50
|
+
const { callee } = call;
|
|
51
|
+
if (callee.type !== _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression || callee.computed) return void 0;
|
|
52
|
+
if (callee.property.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return void 0;
|
|
53
|
+
return callee.property.name;
|
|
54
|
+
}
|
|
55
|
+
function isChainTop(node) {
|
|
56
|
+
const { parent } = node;
|
|
57
|
+
return parent.type !== _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression || parent.object !== node;
|
|
58
|
+
}
|
|
59
|
+
function collectChain(top) {
|
|
60
|
+
const calls = [];
|
|
61
|
+
let current = top;
|
|
62
|
+
while (current.type === _typescript_eslint_utils.AST_NODE_TYPES.CallExpression && current.callee.type === _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression) {
|
|
63
|
+
calls.push(current);
|
|
64
|
+
current = current.callee.object;
|
|
65
|
+
}
|
|
66
|
+
return calls;
|
|
67
|
+
}
|
|
68
|
+
function chainRoot(top) {
|
|
69
|
+
let current = top;
|
|
70
|
+
while (current.type === _typescript_eslint_utils.AST_NODE_TYPES.CallExpression && current.callee.type === _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression) current = current.callee.object;
|
|
71
|
+
return current;
|
|
72
|
+
}
|
|
73
|
+
function resolveConstInit(sourceCode, identifier) {
|
|
74
|
+
const variable = sourceCode.getScope(identifier).references.find((ref) => ref.identifier === identifier)?.resolved;
|
|
75
|
+
const definition = variable?.defs[0];
|
|
76
|
+
if (definition?.node.type !== _typescript_eslint_utils.AST_NODE_TYPES.VariableDeclarator) return void 0;
|
|
77
|
+
if (variable?.references.some((ref) => ref.isWrite() && !ref.init)) return void 0;
|
|
78
|
+
return definition.node.init ?? void 0;
|
|
79
|
+
}
|
|
80
|
+
function propertyKeyIs(prop, name) {
|
|
81
|
+
if (prop.computed) return false;
|
|
82
|
+
const { key } = prop;
|
|
83
|
+
if (key.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return key.name === name;
|
|
84
|
+
return key.value === name;
|
|
85
|
+
}
|
|
86
|
+
function getProperty(node, name) {
|
|
87
|
+
return node.properties.find((prop) => prop.type === _typescript_eslint_utils.AST_NODE_TYPES.Property && propertyKeyIs(prop, name));
|
|
88
|
+
}
|
|
89
|
+
function unwrapAssertions(expr) {
|
|
90
|
+
let current = expr;
|
|
91
|
+
while (current.type === _typescript_eslint_utils.AST_NODE_TYPES.TSAsExpression || current.type === _typescript_eslint_utils.AST_NODE_TYPES.TSTypeAssertion || current.type === _typescript_eslint_utils.AST_NODE_TYPES.TSSatisfiesExpression) current = current.expression;
|
|
92
|
+
return current;
|
|
93
|
+
}
|
|
94
|
+
const V2_BUILDERS = new Set([
|
|
95
|
+
"ContainerBuilder",
|
|
96
|
+
"SectionBuilder",
|
|
97
|
+
"TextDisplayBuilder",
|
|
98
|
+
"MediaGalleryBuilder",
|
|
99
|
+
"FileBuilder",
|
|
100
|
+
"SeparatorBuilder",
|
|
101
|
+
"ThumbnailBuilder"
|
|
102
|
+
]);
|
|
103
|
+
function isV2Type(type, checker) {
|
|
104
|
+
return extendsDjsType(checker, type, V2_BUILDERS);
|
|
105
|
+
}
|
|
106
|
+
function arrayHoldsV2(type, checker) {
|
|
107
|
+
const elementType = type?.getNumberIndexType();
|
|
108
|
+
return elementType !== void 0 && isV2Type(elementType, checker);
|
|
109
|
+
}
|
|
110
|
+
function componentsValueIsV2(value, services, checker) {
|
|
111
|
+
if (value.type === _typescript_eslint_utils.AST_NODE_TYPES.ArrayExpression) return value.elements.some((element) => {
|
|
112
|
+
if (element === null) return false;
|
|
113
|
+
if (element.type === _typescript_eslint_utils.AST_NODE_TYPES.SpreadElement) return arrayHoldsV2(services.getTypeAtLocation(element.argument), checker);
|
|
114
|
+
return isV2Type(services.getTypeAtLocation(element), checker);
|
|
115
|
+
});
|
|
116
|
+
return arrayHoldsV2(services.getTypeAtLocation(value), checker);
|
|
117
|
+
}
|
|
118
|
+
function hasV2Components(node, services, checker) {
|
|
119
|
+
let holdsV2 = false;
|
|
120
|
+
for (const prop of node.properties) if (prop.type === _typescript_eslint_utils.AST_NODE_TYPES.Property && propertyKeyIs(prop, "components")) holdsV2 = componentsValueIsV2(prop.value, services, checker);
|
|
121
|
+
else if (prop.type === _typescript_eslint_utils.AST_NODE_TYPES.SpreadElement) {
|
|
122
|
+
const symbol = services.getTypeAtLocation(prop.argument).getProperty("components");
|
|
123
|
+
if (symbol !== void 0) holdsV2 = arrayHoldsV2(checker.getTypeOfSymbol(symbol), checker);
|
|
124
|
+
}
|
|
125
|
+
return holdsV2;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
//#endregion
|
|
129
|
+
//#region src/rules/discord/no-choices-and-autocomplete.ts
|
|
130
|
+
const OPTION_BUILDERS = new Set([
|
|
131
|
+
"SlashCommandStringOption",
|
|
132
|
+
"SlashCommandIntegerOption",
|
|
133
|
+
"SlashCommandNumberOption"
|
|
134
|
+
]);
|
|
135
|
+
function autocompleteOn(calls, services, checker) {
|
|
136
|
+
const arg = calls.find((call) => methodName(call) === "setAutocomplete")?.arguments[0];
|
|
137
|
+
if (arg === void 0) return false;
|
|
138
|
+
if (arg.type === _typescript_eslint_utils.AST_NODE_TYPES.Literal) return arg.value === true;
|
|
139
|
+
return booleanLiteralValue(checker, services.getTypeAtLocation(arg)) === true;
|
|
140
|
+
}
|
|
141
|
+
function declaresChoices(calls) {
|
|
142
|
+
let has = false;
|
|
143
|
+
for (const call of [...calls].reverse()) {
|
|
144
|
+
const name = methodName(call);
|
|
145
|
+
if (name === "addChoices") {
|
|
146
|
+
if (call.arguments.some((arg) => arg.type !== _typescript_eslint_utils.AST_NODE_TYPES.SpreadElement)) has = true;
|
|
147
|
+
} else if (name === "setChoices") has = call.arguments.some((arg) => {
|
|
148
|
+
if (arg.type === _typescript_eslint_utils.AST_NODE_TYPES.SpreadElement) return false;
|
|
149
|
+
if (arg.type === _typescript_eslint_utils.AST_NODE_TYPES.ArrayExpression) return arg.elements.some((el) => el !== null && el.type !== _typescript_eslint_utils.AST_NODE_TYPES.SpreadElement);
|
|
150
|
+
return true;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return has;
|
|
154
|
+
}
|
|
155
|
+
var no_choices_and_autocomplete_default = createRule({
|
|
156
|
+
name: "no-choices-and-autocomplete",
|
|
157
|
+
meta: {
|
|
158
|
+
type: "problem",
|
|
159
|
+
docs: { description: "Disallow both autocomplete and choices on the same slash option." },
|
|
160
|
+
messages: { bothSet: "A slash option cannot enable autocomplete and declare choices at once. Building that throws a RangeError." },
|
|
161
|
+
schema: []
|
|
162
|
+
},
|
|
163
|
+
defaultOptions: [],
|
|
164
|
+
create(context) {
|
|
165
|
+
const services = _typescript_eslint_utils.ESLintUtils.getParserServices(context);
|
|
166
|
+
const checker = services.program.getTypeChecker();
|
|
167
|
+
return { CallExpression(node) {
|
|
168
|
+
if (!isChainTop(node)) return;
|
|
169
|
+
if (!extendsDjsType(checker, services.getTypeAtLocation(chainRoot(node)), OPTION_BUILDERS)) return;
|
|
170
|
+
const calls = collectChain(node);
|
|
171
|
+
if (autocompleteOn(calls, services, checker) && declaresChoices(calls)) context.report({
|
|
172
|
+
node,
|
|
173
|
+
messageId: "bothSet"
|
|
174
|
+
});
|
|
175
|
+
} };
|
|
176
|
+
}
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region src/rules/discord/no-conflicting-button-props.ts
|
|
181
|
+
const BUTTON_STYLE_LINK = 5;
|
|
182
|
+
function setsLinkStyle(call, services) {
|
|
183
|
+
if (methodName(call) !== "setStyle") return false;
|
|
184
|
+
const arg = call.arguments[0];
|
|
185
|
+
if (arg === void 0) return false;
|
|
186
|
+
if (arg.type === _typescript_eslint_utils.AST_NODE_TYPES.Literal && arg.value === BUTTON_STYLE_LINK) return true;
|
|
187
|
+
if (arg.type === _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression && arg.object.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier && arg.object.name === "ButtonStyle" && arg.property.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier && arg.property.name === "Link") return true;
|
|
188
|
+
const type = services.getTypeAtLocation(arg);
|
|
189
|
+
return type.isNumberLiteral() && type.value === BUTTON_STYLE_LINK;
|
|
190
|
+
}
|
|
191
|
+
var no_conflicting_button_props_default = createRule({
|
|
192
|
+
name: "no-conflicting-button-props",
|
|
193
|
+
meta: {
|
|
194
|
+
type: "problem",
|
|
195
|
+
docs: { description: "Disallow conflicting props on a button builder." },
|
|
196
|
+
messages: {
|
|
197
|
+
idAndUrl: "A button cannot set both a customId and a url.",
|
|
198
|
+
linkWithCustomId: "A link button uses a url and cannot have a customId."
|
|
199
|
+
},
|
|
200
|
+
schema: []
|
|
201
|
+
},
|
|
202
|
+
defaultOptions: [],
|
|
203
|
+
create(context) {
|
|
204
|
+
const services = _typescript_eslint_utils.ESLintUtils.getParserServices(context);
|
|
205
|
+
const checker = services.program.getTypeChecker();
|
|
206
|
+
return { CallExpression(node) {
|
|
207
|
+
if (!isChainTop(node)) return;
|
|
208
|
+
if (!extendsDjsType(checker, services.getTypeAtLocation(chainRoot(node)), "ButtonBuilder")) return;
|
|
209
|
+
const calls = collectChain(node);
|
|
210
|
+
const hasCustomId = calls.some((call) => methodName(call) === "setCustomId");
|
|
211
|
+
const hasUrl = calls.some((call) => methodName(call) === "setURL");
|
|
212
|
+
if (hasCustomId && hasUrl) context.report({
|
|
213
|
+
node,
|
|
214
|
+
messageId: "idAndUrl"
|
|
215
|
+
});
|
|
216
|
+
else if (hasCustomId && calls.some((call) => setsLinkStyle(call, services))) context.report({
|
|
217
|
+
node,
|
|
218
|
+
messageId: "linkWithCustomId"
|
|
219
|
+
});
|
|
220
|
+
} };
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
//#endregion
|
|
225
|
+
//#region src/rules/discord/no-discord-limit-exceeded.ts
|
|
226
|
+
const LIMITS = [
|
|
227
|
+
{
|
|
228
|
+
builders: new Set(["ActionRowBuilder"]),
|
|
229
|
+
addMethod: "addComponents",
|
|
230
|
+
setMethod: "setComponents",
|
|
231
|
+
cap: 5,
|
|
232
|
+
detail: "An action row holds at most 5 components"
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
builders: new Set(["StringSelectMenuBuilder"]),
|
|
236
|
+
addMethod: "addOptions",
|
|
237
|
+
setMethod: "setOptions",
|
|
238
|
+
cap: 25,
|
|
239
|
+
detail: "A select menu holds at most 25 options"
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
builders: new Set(["EmbedBuilder"]),
|
|
243
|
+
addMethod: "addFields",
|
|
244
|
+
setMethod: "setFields",
|
|
245
|
+
cap: 25,
|
|
246
|
+
detail: "An embed holds at most 25 fields"
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
builders: new Set([
|
|
250
|
+
"SlashCommandStringOption",
|
|
251
|
+
"SlashCommandIntegerOption",
|
|
252
|
+
"SlashCommandNumberOption"
|
|
253
|
+
]),
|
|
254
|
+
addMethod: "addChoices",
|
|
255
|
+
setMethod: "setChoices",
|
|
256
|
+
cap: 25,
|
|
257
|
+
detail: "A slash option holds at most 25 choices"
|
|
258
|
+
}
|
|
259
|
+
];
|
|
260
|
+
function tupleLength(type, checker) {
|
|
261
|
+
if (!checker.isTupleType(type)) return void 0;
|
|
262
|
+
const reference = type;
|
|
263
|
+
const target = reference.target;
|
|
264
|
+
const length = checker.getTypeArguments(reference).length;
|
|
265
|
+
return target.minLength === length ? length : void 0;
|
|
266
|
+
}
|
|
267
|
+
function spreadCount(spread, services, checker) {
|
|
268
|
+
return tupleLength(services.getTypeAtLocation(spread.argument), checker);
|
|
269
|
+
}
|
|
270
|
+
function arrayLength(array, services, checker) {
|
|
271
|
+
let count = 0;
|
|
272
|
+
for (const element of array.elements) if (element?.type === _typescript_eslint_utils.AST_NODE_TYPES.SpreadElement) {
|
|
273
|
+
const arity = spreadCount(element, services, checker);
|
|
274
|
+
if (arity === void 0) return void 0;
|
|
275
|
+
count += arity;
|
|
276
|
+
} else count += 1;
|
|
277
|
+
return count;
|
|
278
|
+
}
|
|
279
|
+
function countStaticItems(calls, limit, services, checker) {
|
|
280
|
+
let count = 0;
|
|
281
|
+
let matched = false;
|
|
282
|
+
for (const call of [...calls].reverse()) {
|
|
283
|
+
const name = methodName(call);
|
|
284
|
+
if (name === limit.addMethod) {
|
|
285
|
+
matched = true;
|
|
286
|
+
for (const arg of call.arguments) if (arg.type === _typescript_eslint_utils.AST_NODE_TYPES.SpreadElement) {
|
|
287
|
+
const arity = spreadCount(arg, services, checker);
|
|
288
|
+
if (arity === void 0) return void 0;
|
|
289
|
+
count += arity;
|
|
290
|
+
} else count += 1;
|
|
291
|
+
} else if (name === limit.setMethod) {
|
|
292
|
+
matched = true;
|
|
293
|
+
const arg = call.arguments[0];
|
|
294
|
+
const length = arg?.type === _typescript_eslint_utils.AST_NODE_TYPES.ArrayExpression ? arrayLength(arg, services, checker) : arg !== void 0 && arg.type !== _typescript_eslint_utils.AST_NODE_TYPES.SpreadElement ? tupleLength(services.getTypeAtLocation(arg), checker) : void 0;
|
|
295
|
+
if (length === void 0) return void 0;
|
|
296
|
+
count = length;
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return matched ? count : void 0;
|
|
300
|
+
}
|
|
301
|
+
var no_discord_limit_exceeded_default = createRule({
|
|
302
|
+
name: "no-discord-limit-exceeded",
|
|
303
|
+
meta: {
|
|
304
|
+
type: "problem",
|
|
305
|
+
docs: { description: "Disallow exceeding a Discord builder limit with a statically-known number of items." },
|
|
306
|
+
messages: { tooMany: "{{detail}}. This chain declares {{count}}." },
|
|
307
|
+
schema: []
|
|
308
|
+
},
|
|
309
|
+
defaultOptions: [],
|
|
310
|
+
create(context) {
|
|
311
|
+
const services = _typescript_eslint_utils.ESLintUtils.getParserServices(context);
|
|
312
|
+
const checker = services.program.getTypeChecker();
|
|
313
|
+
return { CallExpression(node) {
|
|
314
|
+
if (!isChainTop(node)) return;
|
|
315
|
+
const calls = collectChain(node);
|
|
316
|
+
const methods = new Set(calls.map((call) => methodName(call)));
|
|
317
|
+
if (!LIMITS.some((limit) => methods.has(limit.addMethod) || methods.has(limit.setMethod))) return;
|
|
318
|
+
const type = services.getTypeAtLocation(chainRoot(node));
|
|
319
|
+
const limit = LIMITS.find((entry) => extendsDjsType(checker, type, entry.builders));
|
|
320
|
+
if (!limit) return;
|
|
321
|
+
const count = countStaticItems(calls, limit, services, checker);
|
|
322
|
+
if (count !== void 0 && count > limit.cap) context.report({
|
|
323
|
+
node,
|
|
324
|
+
messageId: "tooMany",
|
|
325
|
+
data: {
|
|
326
|
+
detail: limit.detail,
|
|
327
|
+
count
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
} };
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
//#endregion
|
|
335
|
+
//#region src/rules/discord/no-mixed-message-format.ts
|
|
336
|
+
const CONTENT_FIELDS = [
|
|
337
|
+
"content",
|
|
338
|
+
"embeds",
|
|
339
|
+
"poll",
|
|
340
|
+
"stickers",
|
|
341
|
+
"sticker_ids"
|
|
342
|
+
];
|
|
343
|
+
function isDefinitelyUndefined(type) {
|
|
344
|
+
return (type.flags & typescript.TypeFlags.Undefined) !== 0;
|
|
345
|
+
}
|
|
346
|
+
var no_mixed_message_format_default = createRule({
|
|
347
|
+
name: "no-mixed-message-format",
|
|
348
|
+
meta: {
|
|
349
|
+
type: "problem",
|
|
350
|
+
docs: { description: "Disallow a message that mixes builder components with content, embeds, poll, or stickers." },
|
|
351
|
+
messages: { mixedFormat: "A message cannot mix builder components with content, embeds, poll, or stickers. Discord rejects the payload." },
|
|
352
|
+
schema: []
|
|
353
|
+
},
|
|
354
|
+
defaultOptions: [],
|
|
355
|
+
create(context) {
|
|
356
|
+
const services = _typescript_eslint_utils.ESLintUtils.getParserServices(context);
|
|
357
|
+
const checker = services.program.getTypeChecker();
|
|
358
|
+
function spreadHasContent(node) {
|
|
359
|
+
for (const prop of node.properties) {
|
|
360
|
+
if (prop.type !== _typescript_eslint_utils.AST_NODE_TYPES.SpreadElement) continue;
|
|
361
|
+
const type = services.getTypeAtLocation(prop.argument);
|
|
362
|
+
if (CONTENT_FIELDS.some((name) => {
|
|
363
|
+
const symbol = type.getProperty(name);
|
|
364
|
+
if (symbol === void 0 || (symbol.flags & typescript.SymbolFlags.Optional) !== 0) return false;
|
|
365
|
+
return !isDefinitelyUndefined(checker.getTypeOfSymbol(symbol));
|
|
366
|
+
})) return true;
|
|
367
|
+
}
|
|
368
|
+
return false;
|
|
369
|
+
}
|
|
370
|
+
function hasContentField(node) {
|
|
371
|
+
return CONTENT_FIELDS.some((name) => {
|
|
372
|
+
const prop = getProperty(node, name);
|
|
373
|
+
return prop !== void 0 && !isDefinitelyUndefined(services.getTypeAtLocation(prop.value));
|
|
374
|
+
}) || spreadHasContent(node);
|
|
375
|
+
}
|
|
376
|
+
return { ObjectExpression(node) {
|
|
377
|
+
if (hasV2Components(node, services, checker) && hasContentField(node)) context.report({
|
|
378
|
+
node,
|
|
379
|
+
messageId: "mixedFormat"
|
|
380
|
+
});
|
|
381
|
+
} };
|
|
382
|
+
}
|
|
383
|
+
});
|
|
384
|
+
|
|
385
|
+
//#endregion
|
|
386
|
+
//#region src/rules/discord/prefer-ephemeral-flag.ts
|
|
387
|
+
const REPLY_METHODS = new Set([
|
|
388
|
+
"reply",
|
|
389
|
+
"deferReply",
|
|
390
|
+
"followUp"
|
|
391
|
+
]);
|
|
392
|
+
function propertyName(property) {
|
|
393
|
+
if (property.type !== _typescript_eslint_utils.AST_NODE_TYPES.Property || property.computed) return void 0;
|
|
394
|
+
if (property.key.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return property.key.name;
|
|
395
|
+
return typeof property.key.value === "string" ? property.key.value : void 0;
|
|
396
|
+
}
|
|
397
|
+
function resolveOptions(sourceCode, arg) {
|
|
398
|
+
if (arg?.type === _typescript_eslint_utils.AST_NODE_TYPES.ObjectExpression) return arg;
|
|
399
|
+
if (arg?.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return void 0;
|
|
400
|
+
const init = resolveConstInit(sourceCode, arg);
|
|
401
|
+
return init?.type === _typescript_eslint_utils.AST_NODE_TYPES.ObjectExpression ? init : void 0;
|
|
402
|
+
}
|
|
403
|
+
function canReplaceEphemeral(ephemeral, options, messageFlagsAlias) {
|
|
404
|
+
const hasFlags = options.properties.some((property) => propertyName(property) === "flags");
|
|
405
|
+
const hasSpread = options.properties.some((property) => property.type === _typescript_eslint_utils.AST_NODE_TYPES.SpreadElement);
|
|
406
|
+
const isTrue = ephemeral.value.type === _typescript_eslint_utils.AST_NODE_TYPES.Literal && ephemeral.value.value === true;
|
|
407
|
+
return messageFlagsAlias !== void 0 && !hasFlags && !hasSpread && isTrue;
|
|
408
|
+
}
|
|
409
|
+
function destructuredReply(node, sourceCode) {
|
|
410
|
+
if (node.callee.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return void 0;
|
|
411
|
+
const calleeId = node.callee;
|
|
412
|
+
const def = (sourceCode.getScope(node).references.find((ref) => ref.identifier === calleeId)?.resolved)?.defs[0];
|
|
413
|
+
if (def?.node.type !== _typescript_eslint_utils.AST_NODE_TYPES.VariableDeclarator) return void 0;
|
|
414
|
+
const { id: pattern, init } = def.node;
|
|
415
|
+
if (pattern.type !== _typescript_eslint_utils.AST_NODE_TYPES.ObjectPattern || !init) return void 0;
|
|
416
|
+
const prop = pattern.properties.find((p) => p.type === _typescript_eslint_utils.AST_NODE_TYPES.Property && !p.computed && p.key.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier && p.value.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier && p.value.name === calleeId.name);
|
|
417
|
+
if (prop?.key.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier) return void 0;
|
|
418
|
+
return {
|
|
419
|
+
name: prop.key.name,
|
|
420
|
+
init
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
var prefer_ephemeral_flag_default = createRule({
|
|
424
|
+
name: "prefer-ephemeral-flag",
|
|
425
|
+
meta: {
|
|
426
|
+
type: "suggestion",
|
|
427
|
+
fixable: "code",
|
|
428
|
+
docs: { description: "Disallow the deprecated ephemeral reply option in favor of MessageFlags.Ephemeral." },
|
|
429
|
+
messages: { deprecated: "The ephemeral reply option is deprecated. Use flags: MessageFlags.Ephemeral." },
|
|
430
|
+
schema: []
|
|
431
|
+
},
|
|
432
|
+
defaultOptions: [],
|
|
433
|
+
create(context) {
|
|
434
|
+
const services = _typescript_eslint_utils.ESLintUtils.getParserServices(context);
|
|
435
|
+
const checker = services.program.getTypeChecker();
|
|
436
|
+
let messageFlagsAlias;
|
|
437
|
+
return {
|
|
438
|
+
ImportDeclaration(node) {
|
|
439
|
+
if (node.source.value !== "discord.js") return;
|
|
440
|
+
for (const spec of node.specifiers) if (spec.type === _typescript_eslint_utils.AST_NODE_TYPES.ImportSpecifier && spec.imported.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier && spec.imported.name === "MessageFlags") messageFlagsAlias = spec.local.name;
|
|
441
|
+
},
|
|
442
|
+
CallExpression(node) {
|
|
443
|
+
let name;
|
|
444
|
+
let receiverType;
|
|
445
|
+
if (node.callee.type === _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression) {
|
|
446
|
+
name = methodName(node);
|
|
447
|
+
receiverType = services.getTypeAtLocation(node.callee.object);
|
|
448
|
+
} else {
|
|
449
|
+
const reply = destructuredReply(node, context.sourceCode);
|
|
450
|
+
name = reply?.name;
|
|
451
|
+
receiverType = reply ? services.getTypeAtLocation(reply.init) : void 0;
|
|
452
|
+
}
|
|
453
|
+
if (name === void 0 || !REPLY_METHODS.has(name) || receiverType === void 0) return;
|
|
454
|
+
if (!extendsDjsType(checker, receiverType, "BaseInteraction")) return;
|
|
455
|
+
const options = resolveOptions(context.sourceCode, node.arguments[0]);
|
|
456
|
+
if (options === void 0) return;
|
|
457
|
+
const ephemeral = options.properties.find((property) => propertyName(property) === "ephemeral");
|
|
458
|
+
if (ephemeral?.type !== _typescript_eslint_utils.AST_NODE_TYPES.Property) return;
|
|
459
|
+
const canFix = canReplaceEphemeral(ephemeral, options, messageFlagsAlias);
|
|
460
|
+
context.report({
|
|
461
|
+
node: ephemeral,
|
|
462
|
+
messageId: "deprecated",
|
|
463
|
+
fix: canFix ? (fixer) => fixer.replaceText(ephemeral, `flags: ${messageFlagsAlias}.Ephemeral`) : null
|
|
464
|
+
});
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
});
|
|
469
|
+
|
|
470
|
+
//#endregion
|
|
471
|
+
//#region src/rules/discord/prefer-v2-component.ts
|
|
472
|
+
var prefer_v2_component_default = createRule({
|
|
473
|
+
name: "prefer-v2-component",
|
|
474
|
+
meta: {
|
|
475
|
+
type: "suggestion",
|
|
476
|
+
docs: { description: "Prefer a components v2 layout over a legacy embed." },
|
|
477
|
+
messages: { preferV2: "Prefer a components v2 layout (ContainerBuilder, TextDisplayBuilder) over an embed." },
|
|
478
|
+
schema: []
|
|
479
|
+
},
|
|
480
|
+
defaultOptions: [],
|
|
481
|
+
create(context) {
|
|
482
|
+
const services = _typescript_eslint_utils.ESLintUtils.getParserServices(context);
|
|
483
|
+
const checker = services.program.getTypeChecker();
|
|
484
|
+
return {
|
|
485
|
+
NewExpression(node) {
|
|
486
|
+
if (extendsDjsType(checker, services.getTypeAtLocation(node), "EmbedBuilder")) context.report({
|
|
487
|
+
node,
|
|
488
|
+
messageId: "preferV2"
|
|
489
|
+
});
|
|
490
|
+
},
|
|
491
|
+
CallExpression(node) {
|
|
492
|
+
if (node.callee.type !== _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression) return;
|
|
493
|
+
if (node.callee.property.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier || node.callee.property.name !== "from") return;
|
|
494
|
+
if (extendsDjsType(checker, services.getTypeAtLocation(node.callee.object), "EmbedBuilder")) context.report({
|
|
495
|
+
node,
|
|
496
|
+
messageId: "preferV2"
|
|
497
|
+
});
|
|
498
|
+
},
|
|
499
|
+
ClassDeclaration(node) {
|
|
500
|
+
if (!node.superClass || !node.id) return;
|
|
501
|
+
const symbol = services.getSymbolAtLocation(node.id);
|
|
502
|
+
if (!symbol) return;
|
|
503
|
+
const classType = checker.getDeclaredTypeOfSymbol(symbol);
|
|
504
|
+
if (!extendsSeedcordType(checker, classType, "BuilderComponent")) return;
|
|
505
|
+
const component = classType.getProperty("component");
|
|
506
|
+
if (!component) return;
|
|
507
|
+
const componentSymbol = checker.getTypeOfSymbolAtLocation(component, services.esTreeNodeToTSNodeMap.get(node)).getSymbol();
|
|
508
|
+
if (componentSymbol?.getName() === "EmbedBuilder" && isFromDiscordJs(componentSymbol)) context.report({
|
|
509
|
+
node: node.id,
|
|
510
|
+
messageId: "preferV2"
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
//#endregion
|
|
518
|
+
//#region src/rules/discord/require-components-v2-flag.ts
|
|
519
|
+
const IS_COMPONENTS_V2 = 32768;
|
|
520
|
+
function combine(states) {
|
|
521
|
+
const [first] = states;
|
|
522
|
+
if (first === void 0) return "unknown";
|
|
523
|
+
return states.every((state) => state === first) ? first : "unknown";
|
|
524
|
+
}
|
|
525
|
+
function bitState(value) {
|
|
526
|
+
return (value & IS_COMPONENTS_V2) === 0 ? "absent" : "present";
|
|
527
|
+
}
|
|
528
|
+
function flagTypeState(type) {
|
|
529
|
+
if (type === void 0) return "unknown";
|
|
530
|
+
if (type.isUnion()) return combine(type.types.map(flagTypeState));
|
|
531
|
+
if (type.isNumberLiteral()) return bitState(type.value);
|
|
532
|
+
if (type.isStringLiteral()) {
|
|
533
|
+
if (type.value === "IsComponentsV2") return "present";
|
|
534
|
+
if (/^\d+$/u.test(type.value)) return bitState(Number(type.value));
|
|
535
|
+
return "absent";
|
|
536
|
+
}
|
|
537
|
+
const element = type.getNumberIndexType();
|
|
538
|
+
if (element !== void 0) return flagTypeState(element);
|
|
539
|
+
return "unknown";
|
|
540
|
+
}
|
|
541
|
+
function applyBitwise(operator, left, right) {
|
|
542
|
+
switch (operator) {
|
|
543
|
+
case "|": return left | right;
|
|
544
|
+
case "&": return left & right;
|
|
545
|
+
case "^": return left ^ right;
|
|
546
|
+
case "<<": return left << right;
|
|
547
|
+
case ">>": return left >> right;
|
|
548
|
+
case ">>>": return left >>> right;
|
|
549
|
+
default: return;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
function foldToNumber(node, services) {
|
|
553
|
+
switch (node.type) {
|
|
554
|
+
case _typescript_eslint_utils.AST_NODE_TYPES.Literal: return typeof node.value === "number" ? node.value : void 0;
|
|
555
|
+
case _typescript_eslint_utils.AST_NODE_TYPES.TSAsExpression: return foldToNumber(node.expression, services);
|
|
556
|
+
case _typescript_eslint_utils.AST_NODE_TYPES.UnaryExpression: {
|
|
557
|
+
const value = foldToNumber(node.argument, services);
|
|
558
|
+
if (value === void 0) return void 0;
|
|
559
|
+
if (node.operator === "-") return -value;
|
|
560
|
+
if (node.operator === "~") return ~value;
|
|
561
|
+
if (node.operator === "+") return value;
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
case _typescript_eslint_utils.AST_NODE_TYPES.BinaryExpression: {
|
|
565
|
+
const left = foldToNumber(node.left, services);
|
|
566
|
+
const right = foldToNumber(node.right, services);
|
|
567
|
+
if (left === void 0 || right === void 0) return void 0;
|
|
568
|
+
return applyBitwise(node.operator, left, right);
|
|
569
|
+
}
|
|
570
|
+
case _typescript_eslint_utils.AST_NODE_TYPES.Identifier:
|
|
571
|
+
case _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression: {
|
|
572
|
+
const type = services.getTypeAtLocation(node);
|
|
573
|
+
return type.isNumberLiteral() ? type.value : void 0;
|
|
574
|
+
}
|
|
575
|
+
default: return;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
function elementState(node, services) {
|
|
579
|
+
const folded = foldToNumber(node, services);
|
|
580
|
+
if (folded !== void 0) return bitState(folded);
|
|
581
|
+
return flagTypeState(services.getTypeAtLocation(node));
|
|
582
|
+
}
|
|
583
|
+
function flagValueState(value, services) {
|
|
584
|
+
if (value.type === _typescript_eslint_utils.AST_NODE_TYPES.ArrayExpression) {
|
|
585
|
+
const states = new Set(value.elements.map((element) => {
|
|
586
|
+
if (element === null) return "absent";
|
|
587
|
+
if (element.type === _typescript_eslint_utils.AST_NODE_TYPES.SpreadElement) return flagTypeState(services.getTypeAtLocation(element.argument).getNumberIndexType());
|
|
588
|
+
return elementState(element, services);
|
|
589
|
+
}));
|
|
590
|
+
if (states.has("present")) return "present";
|
|
591
|
+
return states.has("unknown") ? "unknown" : "absent";
|
|
592
|
+
}
|
|
593
|
+
return elementState(value, services);
|
|
594
|
+
}
|
|
595
|
+
function flagState(node, services, checker) {
|
|
596
|
+
let state = "absent";
|
|
597
|
+
for (const prop of node.properties) if (prop.type === _typescript_eslint_utils.AST_NODE_TYPES.Property && propertyKeyIs(prop, "flags")) state = flagValueState(prop.value, services);
|
|
598
|
+
else if (prop.type === _typescript_eslint_utils.AST_NODE_TYPES.SpreadElement) {
|
|
599
|
+
const symbol = services.getTypeAtLocation(prop.argument).getProperty("flags");
|
|
600
|
+
if (symbol !== void 0) state = flagTypeState(checker.getTypeOfSymbol(symbol));
|
|
601
|
+
}
|
|
602
|
+
return state;
|
|
603
|
+
}
|
|
604
|
+
function isMessageOptionsType(type) {
|
|
605
|
+
if (type.isUnion()) return type.types.some(isMessageOptionsType);
|
|
606
|
+
const components = type.getProperty("components");
|
|
607
|
+
const flags = type.getProperty("flags");
|
|
608
|
+
return components !== void 0 && flags !== void 0 && isFromDiscordJs(components) && isFromDiscordJs(flags);
|
|
609
|
+
}
|
|
610
|
+
var require_components_v2_flag_default = createRule({
|
|
611
|
+
name: "require-components-v2-flag",
|
|
612
|
+
meta: {
|
|
613
|
+
type: "problem",
|
|
614
|
+
docs: { description: "Require the IsComponentsV2 flag on a message that uses v2 builder components." },
|
|
615
|
+
messages: { missingFlag: "A message using v2 builder components must set flags: MessageFlags.IsComponentsV2. Discord rejects the payload otherwise." },
|
|
616
|
+
schema: []
|
|
617
|
+
},
|
|
618
|
+
defaultOptions: [],
|
|
619
|
+
create(context) {
|
|
620
|
+
const services = _typescript_eslint_utils.ESLintUtils.getParserServices(context);
|
|
621
|
+
const checker = services.program.getTypeChecker();
|
|
622
|
+
const reportedInits = /* @__PURE__ */ new Set();
|
|
623
|
+
function payloadViolates(node) {
|
|
624
|
+
return hasV2Components(node, services, checker) && flagState(node, services, checker) === "absent";
|
|
625
|
+
}
|
|
626
|
+
function contextualType(node) {
|
|
627
|
+
return checker.getContextualType(services.esTreeNodeToTSNodeMap.get(node));
|
|
628
|
+
}
|
|
629
|
+
return {
|
|
630
|
+
ObjectExpression(node) {
|
|
631
|
+
if (!payloadViolates(node)) return;
|
|
632
|
+
const contextual = contextualType(node);
|
|
633
|
+
if (contextual === void 0 || !isMessageOptionsType(contextual)) return;
|
|
634
|
+
context.report({
|
|
635
|
+
node,
|
|
636
|
+
messageId: "missingFlag"
|
|
637
|
+
});
|
|
638
|
+
},
|
|
639
|
+
CallExpression(node) {
|
|
640
|
+
for (const arg of node.arguments) {
|
|
641
|
+
if (arg.type !== _typescript_eslint_utils.AST_NODE_TYPES.Identifier) continue;
|
|
642
|
+
const init = resolveConstInit(context.sourceCode, arg);
|
|
643
|
+
if (init?.type !== _typescript_eslint_utils.AST_NODE_TYPES.ObjectExpression) continue;
|
|
644
|
+
if (contextualType(init) !== void 0) continue;
|
|
645
|
+
if (reportedInits.has(init)) continue;
|
|
646
|
+
const contextual = contextualType(arg);
|
|
647
|
+
if (contextual === void 0 || !isMessageOptionsType(contextual)) continue;
|
|
648
|
+
if (payloadViolates(init)) {
|
|
649
|
+
reportedInits.add(init);
|
|
650
|
+
context.report({
|
|
651
|
+
node: init,
|
|
652
|
+
messageId: "missingFlag"
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
});
|
|
660
|
+
|
|
661
|
+
//#endregion
|
|
662
|
+
//#region src/rules/discord/required-option-before-optional.ts
|
|
663
|
+
const SLASH_COMMAND_BUILDERS = new Set(["SlashCommandBuilder", "SlashCommandSubcommandBuilder"]);
|
|
664
|
+
const ADD_OPTION = new Set([
|
|
665
|
+
"addStringOption",
|
|
666
|
+
"addIntegerOption",
|
|
667
|
+
"addBooleanOption",
|
|
668
|
+
"addUserOption",
|
|
669
|
+
"addChannelOption",
|
|
670
|
+
"addRoleOption",
|
|
671
|
+
"addMentionableOption",
|
|
672
|
+
"addNumberOption",
|
|
673
|
+
"addAttachmentOption"
|
|
674
|
+
]);
|
|
675
|
+
function optionChain(callback) {
|
|
676
|
+
if (callback?.type !== _typescript_eslint_utils.AST_NODE_TYPES.ArrowFunctionExpression && callback?.type !== _typescript_eslint_utils.AST_NODE_TYPES.FunctionExpression) return;
|
|
677
|
+
if (callback.body.type !== _typescript_eslint_utils.AST_NODE_TYPES.BlockStatement) return callback.body;
|
|
678
|
+
for (const statement of callback.body.body) if (statement.type === _typescript_eslint_utils.AST_NODE_TYPES.ReturnStatement) return statement.argument ?? void 0;
|
|
679
|
+
}
|
|
680
|
+
function optionRequiredState(callback, services, checker) {
|
|
681
|
+
const chain = optionChain(callback);
|
|
682
|
+
if (chain?.type !== _typescript_eslint_utils.AST_NODE_TYPES.CallExpression) return "unknown";
|
|
683
|
+
const setRequired = collectChain(chain).find((call) => methodName(call) === "setRequired");
|
|
684
|
+
if (!setRequired) return "optional";
|
|
685
|
+
const arg = setRequired.arguments[0];
|
|
686
|
+
if (arg?.type === _typescript_eslint_utils.AST_NODE_TYPES.Literal && typeof arg.value === "boolean") return arg.value ? "required" : "optional";
|
|
687
|
+
if (arg === void 0 || arg.type === _typescript_eslint_utils.AST_NODE_TYPES.SpreadElement) return "unknown";
|
|
688
|
+
const value = booleanLiteralValue(checker, services.getTypeAtLocation(arg));
|
|
689
|
+
if (value === void 0) return "unknown";
|
|
690
|
+
return value ? "required" : "optional";
|
|
691
|
+
}
|
|
692
|
+
var required_option_before_optional_default = createRule({
|
|
693
|
+
name: "required-option-before-optional",
|
|
694
|
+
meta: {
|
|
695
|
+
type: "problem",
|
|
696
|
+
docs: { description: "Disallow a required slash option after an optional one." },
|
|
697
|
+
messages: { outOfOrder: "A required slash option cannot come after an optional one." },
|
|
698
|
+
schema: []
|
|
699
|
+
},
|
|
700
|
+
defaultOptions: [],
|
|
701
|
+
create(context) {
|
|
702
|
+
const services = _typescript_eslint_utils.ESLintUtils.getParserServices(context);
|
|
703
|
+
const checker = services.program.getTypeChecker();
|
|
704
|
+
return { CallExpression(node) {
|
|
705
|
+
if (!isChainTop(node)) return;
|
|
706
|
+
if (!extendsDjsType(checker, services.getTypeAtLocation(chainRoot(node)), SLASH_COMMAND_BUILDERS)) return;
|
|
707
|
+
const options = collectChain(node).filter((call) => ADD_OPTION.has(methodName(call) ?? "")).reverse().map((call) => ({
|
|
708
|
+
call,
|
|
709
|
+
state: optionRequiredState(call.arguments[0], services, checker)
|
|
710
|
+
}));
|
|
711
|
+
if (options.length < 2) return;
|
|
712
|
+
if (options.some((option) => option.state === "unknown")) return;
|
|
713
|
+
let seenOptional = false;
|
|
714
|
+
for (const { call, state } of options) if (state === "optional") seenOptional = true;
|
|
715
|
+
else if (seenOptional) {
|
|
716
|
+
const target = call.callee.type === _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression ? call.callee.property : call;
|
|
717
|
+
context.report({
|
|
718
|
+
node: target,
|
|
719
|
+
messageId: "outOfOrder"
|
|
720
|
+
});
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
} };
|
|
724
|
+
}
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
//#endregion
|
|
728
|
+
//#region src/rules/discord/select-menu-min-exceeds-max.ts
|
|
729
|
+
function lastCall(calls, name) {
|
|
730
|
+
return calls.find((call) => methodName(call) === name);
|
|
731
|
+
}
|
|
732
|
+
function staticNumber(arg, services) {
|
|
733
|
+
if (arg === void 0 || arg.type === _typescript_eslint_utils.AST_NODE_TYPES.SpreadElement) return void 0;
|
|
734
|
+
const target = unwrapAssertions(arg);
|
|
735
|
+
if (target.type === _typescript_eslint_utils.AST_NODE_TYPES.Literal && typeof target.value === "number") return target.value;
|
|
736
|
+
const type = services.getTypeAtLocation(target);
|
|
737
|
+
return type.isNumberLiteral() ? type.value : void 0;
|
|
738
|
+
}
|
|
739
|
+
var select_menu_min_exceeds_max_default = createRule({
|
|
740
|
+
name: "select-menu-min-exceeds-max",
|
|
741
|
+
meta: {
|
|
742
|
+
type: "problem",
|
|
743
|
+
docs: { description: "Disallow a select menu whose minimum selections exceed its maximum." },
|
|
744
|
+
messages: { minOverMax: "setMinValues({{min}}) is greater than setMaxValues({{max}}). Discord rejects the select menu." },
|
|
745
|
+
schema: []
|
|
746
|
+
},
|
|
747
|
+
defaultOptions: [],
|
|
748
|
+
create(context) {
|
|
749
|
+
const services = _typescript_eslint_utils.ESLintUtils.getParserServices(context);
|
|
750
|
+
const checker = services.program.getTypeChecker();
|
|
751
|
+
return { CallExpression(node) {
|
|
752
|
+
if (!isChainTop(node)) return;
|
|
753
|
+
const calls = collectChain(node);
|
|
754
|
+
const minCall = lastCall(calls, "setMinValues");
|
|
755
|
+
const maxCall = lastCall(calls, "setMaxValues");
|
|
756
|
+
if (minCall === void 0 || maxCall === void 0) return;
|
|
757
|
+
if (!extendsDjsType(checker, services.getTypeAtLocation(chainRoot(node)), "BaseSelectMenuBuilder")) return;
|
|
758
|
+
const min = staticNumber(minCall.arguments[0], services);
|
|
759
|
+
const max = staticNumber(maxCall.arguments[0], services);
|
|
760
|
+
if (min === void 0 || max === void 0 || min <= max) return;
|
|
761
|
+
const target = minCall.callee.type === _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression ? minCall.callee.property : minCall;
|
|
762
|
+
context.report({
|
|
763
|
+
node: target,
|
|
764
|
+
messageId: "minOverMax",
|
|
765
|
+
data: {
|
|
766
|
+
min,
|
|
767
|
+
max
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
} };
|
|
771
|
+
}
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
//#endregion
|
|
775
|
+
//#region src/rules/discord/valid-command-name.ts
|
|
776
|
+
const SLASH_BUILDERS = new Set([
|
|
777
|
+
"SlashCommandBuilder",
|
|
778
|
+
"SlashCommandSubcommandBuilder",
|
|
779
|
+
"SlashCommandSubcommandGroupBuilder",
|
|
780
|
+
"SlashCommandStringOption",
|
|
781
|
+
"SlashCommandIntegerOption",
|
|
782
|
+
"SlashCommandNumberOption",
|
|
783
|
+
"SlashCommandBooleanOption",
|
|
784
|
+
"SlashCommandUserOption",
|
|
785
|
+
"SlashCommandChannelOption",
|
|
786
|
+
"SlashCommandRoleOption",
|
|
787
|
+
"SlashCommandMentionableOption",
|
|
788
|
+
"SlashCommandAttachmentOption"
|
|
789
|
+
]);
|
|
790
|
+
function isValidChatInputName(name) {
|
|
791
|
+
return /^[\p{Ll}\p{Lm}\p{Lo}\p{N}\p{sc=Devanagari}\p{sc=Thai}_-]{1,32}$/u.test(name);
|
|
792
|
+
}
|
|
793
|
+
function staticName(arg) {
|
|
794
|
+
if (arg.type === _typescript_eslint_utils.AST_NODE_TYPES.Literal && typeof arg.value === "string") return arg.value;
|
|
795
|
+
if (arg.type === _typescript_eslint_utils.AST_NODE_TYPES.TemplateLiteral && arg.expressions.length === 0) return arg.quasis[0]?.value.cooked ?? void 0;
|
|
796
|
+
}
|
|
797
|
+
var valid_command_name_default = createRule({
|
|
798
|
+
name: "valid-command-name",
|
|
799
|
+
meta: {
|
|
800
|
+
type: "problem",
|
|
801
|
+
docs: { description: "Enforce Discord chat-input name rules on slash command and option names." },
|
|
802
|
+
messages: { invalidName: "This name is not a valid chat-input name. Use lowercase letters, digits, hyphens, or underscores, 1 to 32 characters." },
|
|
803
|
+
schema: []
|
|
804
|
+
},
|
|
805
|
+
defaultOptions: [],
|
|
806
|
+
create(context) {
|
|
807
|
+
const services = _typescript_eslint_utils.ESLintUtils.getParserServices(context);
|
|
808
|
+
return { CallExpression(node) {
|
|
809
|
+
if (methodName(node) !== "setName") return;
|
|
810
|
+
const arg = node.arguments[0];
|
|
811
|
+
if (arg === void 0) return;
|
|
812
|
+
let name = staticName(arg);
|
|
813
|
+
if (name === void 0 && (arg.type === _typescript_eslint_utils.AST_NODE_TYPES.Identifier || arg.type === _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression)) {
|
|
814
|
+
const argType = services.getTypeAtLocation(arg);
|
|
815
|
+
if (argType.isStringLiteral()) name = argType.value;
|
|
816
|
+
}
|
|
817
|
+
if (name === void 0 || isValidChatInputName(name)) return;
|
|
818
|
+
if (node.callee.type !== _typescript_eslint_utils.AST_NODE_TYPES.MemberExpression) return;
|
|
819
|
+
if (!extendsDjsType(services.program.getTypeChecker(), services.getTypeAtLocation(node.callee.object), SLASH_BUILDERS)) return;
|
|
820
|
+
context.report({
|
|
821
|
+
node: arg,
|
|
822
|
+
messageId: "invalidName"
|
|
823
|
+
});
|
|
824
|
+
} };
|
|
825
|
+
}
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
//#endregion
|
|
829
|
+
//#region src/index.ts
|
|
830
|
+
const rules = {
|
|
831
|
+
"no-choices-and-autocomplete": no_choices_and_autocomplete_default,
|
|
832
|
+
"no-conflicting-button-props": no_conflicting_button_props_default,
|
|
833
|
+
"no-discord-limit-exceeded": no_discord_limit_exceeded_default,
|
|
834
|
+
"no-mixed-message-format": no_mixed_message_format_default,
|
|
835
|
+
"prefer-ephemeral-flag": prefer_ephemeral_flag_default,
|
|
836
|
+
"prefer-v2-component": prefer_v2_component_default,
|
|
837
|
+
"require-components-v2-flag": require_components_v2_flag_default,
|
|
838
|
+
"required-option-before-optional": required_option_before_optional_default,
|
|
839
|
+
"select-menu-min-exceeds-max": select_menu_min_exceeds_max_default,
|
|
840
|
+
"valid-command-name": valid_command_name_default
|
|
841
|
+
};
|
|
842
|
+
const plugin = {
|
|
843
|
+
meta: {
|
|
844
|
+
name: "eslint-plugin-discordjs",
|
|
845
|
+
version: "0.0.1"
|
|
846
|
+
},
|
|
847
|
+
rules
|
|
848
|
+
};
|
|
849
|
+
const WARN_RULES = new Set(["prefer-ephemeral-flag", "prefer-v2-component"]);
|
|
850
|
+
const presetRules = {};
|
|
851
|
+
for (const name of Object.keys(rules)) presetRules[`discordjs/${name}`] = WARN_RULES.has(name) ? "warn" : "error";
|
|
852
|
+
const recommended = {
|
|
853
|
+
plugins: { discordjs: plugin },
|
|
854
|
+
rules: presetRules
|
|
855
|
+
};
|
|
856
|
+
plugin.configs = { recommended };
|
|
857
|
+
|
|
858
|
+
//#endregion
|
|
859
|
+
exports.default = plugin;
|
|
860
|
+
exports.recommended = recommended;
|
|
861
|
+
//# sourceMappingURL=index.cjs.map
|