@fedify/lint 2.0.0-dev.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 +20 -0
- package/README.md +410 -0
- package/deno.json +15 -0
- package/dist/index.cjs +751 -0
- package/dist/index.d.cts +160 -0
- package/dist/index.d.ts +160 -0
- package/dist/index.js +726 -0
- package/package.json +74 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
Object.defineProperty(exports, '__esModule', { value: true });
|
|
2
|
+
//#region rolldown:runtime
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys$1 = __getOwnPropNames(from), i = 0, n = keys$1.length, key; i < n; i++) {
|
|
11
|
+
key = keys$1[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
|
|
24
|
+
//#endregion
|
|
25
|
+
const __fxts_core = __toESM(require("@fxts/core"));
|
|
26
|
+
const __typescript_eslint_parser = __toESM(require("@typescript-eslint/parser"));
|
|
27
|
+
|
|
28
|
+
//#region deno.json
|
|
29
|
+
var name = "@fedify/lint";
|
|
30
|
+
var version = "2.0.0";
|
|
31
|
+
var license = "MIT";
|
|
32
|
+
var exports$1 = { ".": "./src/mod.ts" };
|
|
33
|
+
var imports = {
|
|
34
|
+
"eslint": "npm:eslint@^9.0.0",
|
|
35
|
+
"estree": "npm:@types/estree@^1.0.8"
|
|
36
|
+
};
|
|
37
|
+
var tasks = { "test": "deno test --allow-env" };
|
|
38
|
+
var deno_default = {
|
|
39
|
+
name,
|
|
40
|
+
version,
|
|
41
|
+
license,
|
|
42
|
+
exports: exports$1,
|
|
43
|
+
imports,
|
|
44
|
+
tasks
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
//#endregion
|
|
48
|
+
//#region src/lib/const.ts
|
|
49
|
+
/**
|
|
50
|
+
* Mapping of actor property names to their corresponding Context method names
|
|
51
|
+
* and dispatcher methods.
|
|
52
|
+
* Used by lint rules to validate property existence and correct method usage.
|
|
53
|
+
*/
|
|
54
|
+
const properties = {
|
|
55
|
+
id: {
|
|
56
|
+
name: "id",
|
|
57
|
+
path: ["id"],
|
|
58
|
+
getter: "getActorUri",
|
|
59
|
+
setter: "setActorDispatcher",
|
|
60
|
+
requiresIdentifier: true
|
|
61
|
+
},
|
|
62
|
+
following: {
|
|
63
|
+
name: "following",
|
|
64
|
+
path: ["following"],
|
|
65
|
+
getter: "getFollowingUri",
|
|
66
|
+
setter: "setFollowingDispatcher",
|
|
67
|
+
requiresIdentifier: true
|
|
68
|
+
},
|
|
69
|
+
followers: {
|
|
70
|
+
name: "followers",
|
|
71
|
+
path: ["followers"],
|
|
72
|
+
getter: "getFollowersUri",
|
|
73
|
+
setter: "setFollowersDispatcher",
|
|
74
|
+
requiresIdentifier: true
|
|
75
|
+
},
|
|
76
|
+
outbox: {
|
|
77
|
+
name: "outbox",
|
|
78
|
+
path: ["outbox"],
|
|
79
|
+
getter: "getOutboxUri",
|
|
80
|
+
setter: "setOutboxDispatcher",
|
|
81
|
+
requiresIdentifier: true
|
|
82
|
+
},
|
|
83
|
+
inbox: {
|
|
84
|
+
name: "inbox",
|
|
85
|
+
path: ["inbox"],
|
|
86
|
+
getter: "getInboxUri",
|
|
87
|
+
setter: "setInboxListeners",
|
|
88
|
+
requiresIdentifier: true
|
|
89
|
+
},
|
|
90
|
+
liked: {
|
|
91
|
+
name: "liked",
|
|
92
|
+
path: ["liked"],
|
|
93
|
+
getter: "getLikedUri",
|
|
94
|
+
setter: "setLikedDispatcher",
|
|
95
|
+
requiresIdentifier: true
|
|
96
|
+
},
|
|
97
|
+
featured: {
|
|
98
|
+
name: "featured",
|
|
99
|
+
path: ["featured"],
|
|
100
|
+
getter: "getFeaturedUri",
|
|
101
|
+
setter: "setFeaturedDispatcher",
|
|
102
|
+
requiresIdentifier: true
|
|
103
|
+
},
|
|
104
|
+
featuredTags: {
|
|
105
|
+
name: "featuredTags",
|
|
106
|
+
path: ["featuredTags"],
|
|
107
|
+
getter: "getFeaturedTagsUri",
|
|
108
|
+
setter: "setFeaturedTagsDispatcher",
|
|
109
|
+
requiresIdentifier: true
|
|
110
|
+
},
|
|
111
|
+
sharedInbox: {
|
|
112
|
+
name: "sharedInbox",
|
|
113
|
+
path: ["endpoints", "sharedInbox"],
|
|
114
|
+
getter: "getInboxUri",
|
|
115
|
+
setter: "setInboxListeners",
|
|
116
|
+
requiresIdentifier: false,
|
|
117
|
+
nested: {
|
|
118
|
+
parent: "endpoints",
|
|
119
|
+
wrapper: "Endpoints"
|
|
120
|
+
}
|
|
121
|
+
},
|
|
122
|
+
publicKey: {
|
|
123
|
+
name: "publicKey",
|
|
124
|
+
path: ["publicKey"],
|
|
125
|
+
getter: "getActorKeyPairs",
|
|
126
|
+
setter: "setKeyPairsDispatcher",
|
|
127
|
+
requiresIdentifier: true,
|
|
128
|
+
isKeyProperty: true
|
|
129
|
+
},
|
|
130
|
+
assertionMethod: {
|
|
131
|
+
name: "assertionMethod",
|
|
132
|
+
path: ["assertionMethod"],
|
|
133
|
+
getter: "getActorKeyPairs",
|
|
134
|
+
setter: "setKeyPairsDispatcher",
|
|
135
|
+
requiresIdentifier: true,
|
|
136
|
+
isKeyProperty: true
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* Rule IDs for all Fedify lint rules
|
|
141
|
+
*/
|
|
142
|
+
const RULE_IDS = {
|
|
143
|
+
actorIdRequired: "actor-id-required",
|
|
144
|
+
actorFollowingPropertyRequired: "actor-following-property-required",
|
|
145
|
+
actorFollowersPropertyRequired: "actor-followers-property-required",
|
|
146
|
+
actorOutboxPropertyRequired: "actor-outbox-property-required",
|
|
147
|
+
actorLikedPropertyRequired: "actor-liked-property-required",
|
|
148
|
+
actorFeaturedPropertyRequired: "actor-featured-property-required",
|
|
149
|
+
actorFeaturedTagsPropertyRequired: "actor-featured-tags-property-required",
|
|
150
|
+
actorInboxPropertyRequired: "actor-inbox-property-required",
|
|
151
|
+
actorSharedInboxPropertyRequired: "actor-shared-inbox-property-required",
|
|
152
|
+
actorPublicKeyRequired: "actor-public-key-required",
|
|
153
|
+
actorAssertionMethodRequired: "actor-assertion-method-required",
|
|
154
|
+
actorIdMismatch: "actor-id-mismatch",
|
|
155
|
+
actorFollowingPropertyMismatch: "actor-following-property-mismatch",
|
|
156
|
+
actorFollowersPropertyMismatch: "actor-followers-property-mismatch",
|
|
157
|
+
actorOutboxPropertyMismatch: "actor-outbox-property-mismatch",
|
|
158
|
+
actorLikedPropertyMismatch: "actor-liked-property-mismatch",
|
|
159
|
+
actorFeaturedPropertyMismatch: "actor-featured-property-mismatch",
|
|
160
|
+
actorFeaturedTagsPropertyMismatch: "actor-featured-tags-property-mismatch",
|
|
161
|
+
actorInboxPropertyMismatch: "actor-inbox-property-mismatch",
|
|
162
|
+
actorSharedInboxPropertyMismatch: "actor-shared-inbox-property-mismatch",
|
|
163
|
+
collectionFilteringNotImplemented: "collection-filtering-not-implemented"
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
//#endregion
|
|
167
|
+
//#region src/lib/messages.ts
|
|
168
|
+
/**
|
|
169
|
+
* Generates error message for *-required rules.
|
|
170
|
+
* Used when a property is missing from the actor dispatcher return value.
|
|
171
|
+
*
|
|
172
|
+
* @param name - The property name (e.g., "id", "inbox", "following")
|
|
173
|
+
*/
|
|
174
|
+
const actorPropertyRequired = ({ setter, path, getter, requiresIdentifier = true }) => `When \`${setter}\` is configured, the \`${path.join(".")}\` property is recommended. Use \`${getExpectedCall({
|
|
175
|
+
ctxName: "Context",
|
|
176
|
+
methodName: getter,
|
|
177
|
+
idName: "identifier",
|
|
178
|
+
path: path.join("."),
|
|
179
|
+
requiresIdentifier
|
|
180
|
+
})}\` for the \`${path.join(".")}\` property URI.`;
|
|
181
|
+
/**
|
|
182
|
+
* Generates error message for *-mismatch rules.
|
|
183
|
+
* Used when a property exists but uses the wrong context method.
|
|
184
|
+
*
|
|
185
|
+
* @param propertyName - The property name or path
|
|
186
|
+
* (e.g., "id", "endpoints.sharedInbox")
|
|
187
|
+
* @param expectedCall - The expected method call
|
|
188
|
+
* (e.g., "ctx.getActorUri(identifier)")
|
|
189
|
+
*/
|
|
190
|
+
const actorPropertyMismatch = (context) => `Actor's \`${context.path}\` property must match \`${getExpectedCall(context)}\`. \
|
|
191
|
+
Ensure you're using the correct context method.`;
|
|
192
|
+
const getExpectedCall = ({ ctxName, methodName, requiresIdentifier, idName }) => requiresIdentifier ? `${ctxName}.${methodName}(${idName})` : `${ctxName}.${methodName}()`;
|
|
193
|
+
/**
|
|
194
|
+
* Error message for collection filtering not implemented.
|
|
195
|
+
*/
|
|
196
|
+
const COLLECTION_FILTERING_NOT_IMPLEMENTED_ERROR = "Collection dispatcher should implement filtering to avoid large response payloads. Add a fourth parameter (filter) to handle filtering. See: https://fedify.dev/manual/collections#filtering-by-server";
|
|
197
|
+
|
|
198
|
+
//#endregion
|
|
199
|
+
//#region src/lib/utils.ts
|
|
200
|
+
const eq = (value) => (other) => value === other;
|
|
201
|
+
function cases(pred, ifTrue, ifFalse) {
|
|
202
|
+
return (value) => pred(value) ? ifTrue(value) : ifFalse(value);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
//#endregion
|
|
206
|
+
//#region src/lib/pred.ts
|
|
207
|
+
function allOf(...predicates) {
|
|
208
|
+
return (value) => predicates.every((predicate) => predicate(value));
|
|
209
|
+
}
|
|
210
|
+
const anyOf = (...predicates) => (value) => predicates.some((predicate) => predicate(value));
|
|
211
|
+
/**
|
|
212
|
+
* Checks if a node is of a specific type.
|
|
213
|
+
*/
|
|
214
|
+
const isNodeType = (type) => (node) => (0, __fxts_core.pipe)(node, (0, __fxts_core.prop)("type"), eq(type));
|
|
215
|
+
/**
|
|
216
|
+
* Checks if a node is of a specific name.
|
|
217
|
+
*/
|
|
218
|
+
const isNodeName = (name$1) => (node) => (0, __fxts_core.pipe)(node, (0, __fxts_core.prop)("name"), eq(name$1));
|
|
219
|
+
/**
|
|
220
|
+
* Checks if a node's callee is a MemberExpression.
|
|
221
|
+
*/
|
|
222
|
+
const hasMemberExpressionCallee = (node) => node.callee.type === "MemberExpression";
|
|
223
|
+
/**
|
|
224
|
+
* Checks if a node's callee property is an Identifier.
|
|
225
|
+
*/
|
|
226
|
+
const hasIdentifierProperty = (node) => "callee" in node && "property" in node.callee && "type" in node.callee.property && node.callee.property.type === "Identifier";
|
|
227
|
+
/**
|
|
228
|
+
* Checks if a node's callee property name matches the given method name.
|
|
229
|
+
*/
|
|
230
|
+
const hasMethodName = (methodName) => (node) => "callee" in node && "property" in node.callee && "name" in node.callee.property && node.callee.property.name === methodName;
|
|
231
|
+
/**
|
|
232
|
+
* Checks if a CallExpression has minimum required arguments.
|
|
233
|
+
*/
|
|
234
|
+
const hasMinArguments = (min) => (node) => node.arguments.length >= min;
|
|
235
|
+
/**
|
|
236
|
+
* Checks if an expression is a function (arrow or regular).
|
|
237
|
+
*/
|
|
238
|
+
const isFunction = (expr) => anyOf(isNodeType("ArrowFunctionExpression"), isNodeType("FunctionExpression"))(expr);
|
|
239
|
+
/**
|
|
240
|
+
* Checks if a CallExpression is a setActorDispatcher call with
|
|
241
|
+
* proper structure.
|
|
242
|
+
*/
|
|
243
|
+
const isSetActorDispatcherCall = (node) => allOf(hasMemberExpressionCallee, hasIdentifierProperty, hasMethodName("setActorDispatcher"), hasMinArguments(2))(node);
|
|
244
|
+
/**
|
|
245
|
+
* Checks if a function has at least n parameters.
|
|
246
|
+
*/
|
|
247
|
+
const hasMinParams = (min) => (fn) => fn.params.length >= min;
|
|
248
|
+
|
|
249
|
+
//#endregion
|
|
250
|
+
//#region src/lib/property-checker.ts
|
|
251
|
+
/**
|
|
252
|
+
* Checks if a node has a key with a specific name.
|
|
253
|
+
*/
|
|
254
|
+
const hasKeyName = (propertyName) => (node) => (0, __fxts_core.pipe)(node, (0, __fxts_core.prop)("key"), allOf(isNodeType("Identifier"), (0, __fxts_core.pipeLazy)((0, __fxts_core.prop)("name"), eq(propertyName))));
|
|
255
|
+
/**
|
|
256
|
+
* Checks if a node is a Property with an Identifier key of a specific name.
|
|
257
|
+
*/
|
|
258
|
+
const isPropertyWithName = (propertyName) => (node) => allOf(isNodeType("Property"), hasKeyName(propertyName))(node);
|
|
259
|
+
/**
|
|
260
|
+
* Creates a predicate function that checks if a nested property exists.
|
|
261
|
+
* @param path Array of property names forming the path
|
|
262
|
+
* (e.g., ["endpoints", "sharedInbox"])
|
|
263
|
+
* @returns A predicate function that checks if the nested property exists
|
|
264
|
+
*/
|
|
265
|
+
function createPropertyChecker(checker) {
|
|
266
|
+
const inner = ([first, ...rest]) => (node) => {
|
|
267
|
+
if (!isPropertyWithName(first)(node)) return false;
|
|
268
|
+
if ((0, __fxts_core.isEmpty)(rest)) return checker(node.value);
|
|
269
|
+
if (isNodeType("NewExpression")(node.value)) {
|
|
270
|
+
if (node.value.arguments.length === 0) return false;
|
|
271
|
+
const firstArg = node.value.arguments[0];
|
|
272
|
+
if (!isNodeType("ObjectExpression")(firstArg)) return false;
|
|
273
|
+
return firstArg.properties.some(inner(rest));
|
|
274
|
+
}
|
|
275
|
+
return false;
|
|
276
|
+
};
|
|
277
|
+
return inner;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Checks if an ObjectExpression node contains a property.
|
|
281
|
+
* @param propertyChecker The predicate function to check properties
|
|
282
|
+
* @returns A function that checks the ObjectExpression
|
|
283
|
+
*/
|
|
284
|
+
const checkObjectExpression = (propertyChecker) => (obj) => obj.properties.some(propertyChecker);
|
|
285
|
+
/**
|
|
286
|
+
* Checks if a ConditionalExpression (ternary operator) has the property in
|
|
287
|
+
* both branches.
|
|
288
|
+
* @param propertyChecker The predicate function to check properties
|
|
289
|
+
* @returns A function that checks the ConditionalExpression
|
|
290
|
+
*/
|
|
291
|
+
const checkConditionalExpression = (propertyChecker) => (node) => [node.consequent, node.alternate].every(checkBranchWith(propertyChecker));
|
|
292
|
+
const checkBranchWith = (propertyChecker) => (branch) => (0, __fxts_core.pipe)(branch, cases(isNodeType("ConditionalExpression"), checkConditionalExpression(propertyChecker), (0, __fxts_core.pipeLazy)(extractObjectExpression, cases(__fxts_core.isObject, checkObjectExpression(propertyChecker), (0, __fxts_core.always)(false)))));
|
|
293
|
+
/**
|
|
294
|
+
* Extracts the first argument if it's an ObjectExpression.
|
|
295
|
+
*/
|
|
296
|
+
const extractFirstObjectExpression = (node) => (0, __fxts_core.pipe)(node, (0, __fxts_core.prop)("arguments"), __fxts_core.head, (0, __fxts_core.unless)(isNodeType("ObjectExpression"), (0, __fxts_core.always)(null)));
|
|
297
|
+
/**
|
|
298
|
+
* Extracts ObjectExpression from NewExpression.
|
|
299
|
+
*/
|
|
300
|
+
const extractObjectExpression = cases(isNodeType("NewExpression"), extractFirstObjectExpression, (0, __fxts_core.always)(null));
|
|
301
|
+
/**
|
|
302
|
+
* Checks if a ReturnStatement node contains a property.
|
|
303
|
+
* @param propertyChecker The predicate function to check properties
|
|
304
|
+
* @returns A function that checks the ReturnStatement
|
|
305
|
+
*/
|
|
306
|
+
const checkReturnStatement = (propertyChecker) => (node) => (0, __fxts_core.pipe)(node, (0, __fxts_core.prop)("argument"), cases(__fxts_core.isObject, checkBranchWith(propertyChecker), (0, __fxts_core.always)(false)));
|
|
307
|
+
/**
|
|
308
|
+
* Creates a function that recursively checks for a property in an AST node.
|
|
309
|
+
* @param propertyChecker The predicate function to check properties
|
|
310
|
+
* @returns A recursive function that checks the AST node
|
|
311
|
+
*/
|
|
312
|
+
const createPropertySearcher = (propertyChecker) => {
|
|
313
|
+
return (node) => {
|
|
314
|
+
switch (node.type) {
|
|
315
|
+
case "ReturnStatement": return checkReturnStatement(propertyChecker)(node);
|
|
316
|
+
case "BlockStatement": return checkAllReturnPaths(propertyChecker)(node);
|
|
317
|
+
case "NewExpression": return (0, __fxts_core.pipe)(node, extractFirstObjectExpression, (0, __fxts_core.when)(__fxts_core.isObject, checkObjectExpression(propertyChecker)), Boolean);
|
|
318
|
+
default: return false;
|
|
319
|
+
}
|
|
320
|
+
};
|
|
321
|
+
};
|
|
322
|
+
const checkAllReturnPaths = (propertyChecker) => (node) => (0, __fxts_core.pipe)(node, collectReturnPaths, cases(__fxts_core.isEmpty, (0, __fxts_core.always)(false), (0, __fxts_core.every)(checkReturnStatement(propertyChecker))));
|
|
323
|
+
/**
|
|
324
|
+
* Collects all return statements from a node, traversing control flow.
|
|
325
|
+
* This handles if/else branches, loops, etc.
|
|
326
|
+
*/
|
|
327
|
+
const collectReturnPaths = (node) => (0, __fxts_core.pipe)(node, flatten, __fxts_core.toArray);
|
|
328
|
+
function* flatten(node) {
|
|
329
|
+
switch (node.type) {
|
|
330
|
+
case "ReturnStatement":
|
|
331
|
+
yield node;
|
|
332
|
+
return;
|
|
333
|
+
case "IfStatement":
|
|
334
|
+
if (node.consequent) yield* flatten(node.consequent);
|
|
335
|
+
if (node.alternate) yield* flatten(node.alternate);
|
|
336
|
+
return;
|
|
337
|
+
case "BlockStatement":
|
|
338
|
+
for (const statement of node.body) yield* flatten(statement);
|
|
339
|
+
return;
|
|
340
|
+
case "SwitchStatement":
|
|
341
|
+
for (const switchCase of node.cases) for (const statement of switchCase.consequent) yield* flatten(statement);
|
|
342
|
+
return;
|
|
343
|
+
case "TryStatement":
|
|
344
|
+
yield* flatten(node.block);
|
|
345
|
+
if (node.handler) yield* flatten(node.handler.body);
|
|
346
|
+
if (node.finalizer) yield* flatten(node.finalizer);
|
|
347
|
+
return;
|
|
348
|
+
case "WhileStatement":
|
|
349
|
+
case "DoWhileStatement":
|
|
350
|
+
case "ForStatement":
|
|
351
|
+
case "ForInStatement":
|
|
352
|
+
case "ForOfStatement":
|
|
353
|
+
yield* flatten(node.body);
|
|
354
|
+
return;
|
|
355
|
+
case "LabeledStatement":
|
|
356
|
+
yield* flatten(node.body);
|
|
357
|
+
return;
|
|
358
|
+
case "WithStatement":
|
|
359
|
+
yield* flatten(node.body);
|
|
360
|
+
return;
|
|
361
|
+
default: return;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
//#endregion
|
|
366
|
+
//#region src/lib/tracker.ts
|
|
367
|
+
/**
|
|
368
|
+
* Helper to track variable names that store the result of createFederation() or createFederationBuilder() calls
|
|
369
|
+
*/
|
|
370
|
+
function trackFederationVariables() {
|
|
371
|
+
const federationVariables = /* @__PURE__ */ new Set();
|
|
372
|
+
const isFederationObject = (obj) => {
|
|
373
|
+
switch (obj.type) {
|
|
374
|
+
case "Identifier": return federationVariables.has(obj.name);
|
|
375
|
+
case "CallExpression":
|
|
376
|
+
if (isCreateFederation(obj)) return true;
|
|
377
|
+
if (obj.callee.type === "MemberExpression") return isFederationObject(obj.callee.object);
|
|
378
|
+
return false;
|
|
379
|
+
case "MemberExpression": return isFederationObject(obj.object);
|
|
380
|
+
}
|
|
381
|
+
return false;
|
|
382
|
+
};
|
|
383
|
+
return {
|
|
384
|
+
VariableDeclarator(node) {
|
|
385
|
+
const init = node.init;
|
|
386
|
+
const id = node.id;
|
|
387
|
+
if (init?.type === "CallExpression") {
|
|
388
|
+
if (isCreateFederation(init) && id.type === "Identifier") federationVariables.add(id.name);
|
|
389
|
+
}
|
|
390
|
+
},
|
|
391
|
+
isFederationVariable(name$1) {
|
|
392
|
+
return federationVariables.has(name$1);
|
|
393
|
+
},
|
|
394
|
+
isFederationObject
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
const isCreateFederation = (node) => node.callee.type === "Identifier" && /^create(Federation|FederationBuilder)$/i.test(node.callee.name);
|
|
398
|
+
|
|
399
|
+
//#endregion
|
|
400
|
+
//#region src/lib/required.ts
|
|
401
|
+
/**
|
|
402
|
+
* Checks if a CallExpression is a specific dispatcher method call.
|
|
403
|
+
*/
|
|
404
|
+
const isDispatcherMethodCall = (methodName) => (node) => allOf(hasMemberExpressionCallee, hasIdentifierProperty, hasMethodName(methodName))(node);
|
|
405
|
+
/**
|
|
406
|
+
* Tracks dispatcher method calls on federation objects.
|
|
407
|
+
*/
|
|
408
|
+
const createDispatcherTracker = (dispatcherMethod, federationTracker) => {
|
|
409
|
+
let dispatcherConfigured = false;
|
|
410
|
+
return {
|
|
411
|
+
isDispatcherConfigured: () => dispatcherConfigured,
|
|
412
|
+
checkDispatcherCall: (node) => {
|
|
413
|
+
if (isDispatcherMethodCall(dispatcherMethod)(node) && federationTracker.isFederationObject(node.callee.object)) dispatcherConfigured = true;
|
|
414
|
+
}
|
|
415
|
+
};
|
|
416
|
+
};
|
|
417
|
+
function createRequiredRule(config, description) {
|
|
418
|
+
return (context) => {
|
|
419
|
+
const federationTracker = trackFederationVariables();
|
|
420
|
+
const dispatcherTracker = createDispatcherTracker(config.setter, federationTracker);
|
|
421
|
+
const actorDispatchers = [];
|
|
422
|
+
const propertyChecker = createPropertyChecker(Boolean)(config.path);
|
|
423
|
+
const propertySearcher = createPropertySearcher(propertyChecker);
|
|
424
|
+
return {
|
|
425
|
+
VariableDeclarator: federationTracker.VariableDeclarator,
|
|
426
|
+
CallExpression(node) {
|
|
427
|
+
dispatcherTracker.checkDispatcherCall(node);
|
|
428
|
+
if (!isSetActorDispatcherCall(node)) return;
|
|
429
|
+
if (!federationTracker.isFederationObject(node.callee.object)) return;
|
|
430
|
+
const dispatcher = node.arguments[1];
|
|
431
|
+
if (isFunction(dispatcher)) actorDispatchers.push(dispatcher);
|
|
432
|
+
},
|
|
433
|
+
"Program:exit"() {
|
|
434
|
+
if (!dispatcherTracker.isDispatcherConfigured()) return;
|
|
435
|
+
for (const dispatcher of actorDispatchers) if (!propertySearcher(dispatcher.body)) context.report({
|
|
436
|
+
node: dispatcher,
|
|
437
|
+
...description
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
};
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
/**
|
|
444
|
+
* Creates a required rule with the given property configuration.
|
|
445
|
+
*/
|
|
446
|
+
function createRequiredRuleDeno(config) {
|
|
447
|
+
return { create: createRequiredRule(config, { message: actorPropertyRequired(config) }) };
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Creates a required ESLint rule with the given property configuration.
|
|
451
|
+
*/
|
|
452
|
+
function createRequiredRuleEslint(config) {
|
|
453
|
+
return {
|
|
454
|
+
meta: {
|
|
455
|
+
type: "suggestion",
|
|
456
|
+
docs: { description: `Ensure actor dispatcher returns ${config.path.join(".")} property.` },
|
|
457
|
+
schema: [],
|
|
458
|
+
messages: { required: "{{ message }}" }
|
|
459
|
+
},
|
|
460
|
+
create: createRequiredRule(config, {
|
|
461
|
+
messageId: "required",
|
|
462
|
+
data: { message: actorPropertyRequired(config) }
|
|
463
|
+
})
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
//#endregion
|
|
468
|
+
//#region src/rules/actor-assertion-method-required.ts
|
|
469
|
+
const deno$19 = createRequiredRuleDeno(properties.assertionMethod);
|
|
470
|
+
const eslint = createRequiredRuleEslint(properties.assertionMethod);
|
|
471
|
+
|
|
472
|
+
//#endregion
|
|
473
|
+
//#region src/lib/mismatch.ts
|
|
474
|
+
const isIdentifierWithName = (name$1) => (node) => allOf(isNodeType("Identifier"), isNodeName(name$1))(node);
|
|
475
|
+
/**
|
|
476
|
+
* Checks if a node is a CallExpression calling the expected context method.
|
|
477
|
+
*/
|
|
478
|
+
const isExpectedMethodCall = ({ ctxName, idName, methodName, requiresIdentifier }) => (node) => {
|
|
479
|
+
if (!isNodeType("CallExpression")(node) || !hasMemberExpressionCallee(node) || !isIdentifierWithName(ctxName)(node.callee.object) || !isIdentifierWithName(methodName)(node.callee.property)) return false;
|
|
480
|
+
if (!requiresIdentifier) return (0, __fxts_core.isEmpty)(node.arguments);
|
|
481
|
+
return allOf((0, __fxts_core.negate)(__fxts_core.isEmpty), (0, __fxts_core.some)(isIdentifierWithName(idName)))(node.arguments);
|
|
482
|
+
};
|
|
483
|
+
/**
|
|
484
|
+
* Extracts parameter names from a function.
|
|
485
|
+
*/
|
|
486
|
+
const extractParams = (fn) => {
|
|
487
|
+
const params = fn.params;
|
|
488
|
+
if (params.length < 2) return [null, null];
|
|
489
|
+
return params.slice(0, 2).map(getNameIfIdentifier);
|
|
490
|
+
};
|
|
491
|
+
const getNameIfIdentifier = (node) => node?.type === "Identifier" ? node.name : null;
|
|
492
|
+
function createMismatchRule(config, describe) {
|
|
493
|
+
return (context) => {
|
|
494
|
+
const tracker = trackFederationVariables();
|
|
495
|
+
return {
|
|
496
|
+
VariableDeclarator: tracker.VariableDeclarator,
|
|
497
|
+
CallExpression(node) {
|
|
498
|
+
if (!isSetActorDispatcherCall(node) || !hasMemberExpressionCallee(node) || !tracker.isFederationObject(node.callee.object)) return;
|
|
499
|
+
const dispatcherArg = node.arguments[1];
|
|
500
|
+
if (!isFunction(dispatcherArg)) return;
|
|
501
|
+
const [ctxName, idName] = extractParams(dispatcherArg);
|
|
502
|
+
if (!ctxName || !idName) return;
|
|
503
|
+
const methodCallContext = {
|
|
504
|
+
path: config.path.join("."),
|
|
505
|
+
ctxName,
|
|
506
|
+
idName,
|
|
507
|
+
methodName: config.getter,
|
|
508
|
+
requiresIdentifier: config.requiresIdentifier
|
|
509
|
+
};
|
|
510
|
+
const existenceChecker = createPropertyChecker(Boolean)(config.path);
|
|
511
|
+
const hasProperty = createPropertySearcher(existenceChecker)(dispatcherArg.body);
|
|
512
|
+
if (!hasProperty) return;
|
|
513
|
+
const propertyChecker = createPropertyChecker(isExpectedMethodCall(methodCallContext))(config.path);
|
|
514
|
+
const propertySearcher = createPropertySearcher(propertyChecker);
|
|
515
|
+
if (!propertySearcher(dispatcherArg.body)) context.report({
|
|
516
|
+
node: dispatcherArg,
|
|
517
|
+
...describe(methodCallContext)
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Creates a lint rule that checks if a property uses
|
|
525
|
+
* the correct context method.
|
|
526
|
+
*
|
|
527
|
+
* @param config Property configuration containing name, getter, setter, and
|
|
528
|
+
* nested info
|
|
529
|
+
* @returns A Deno lint rule
|
|
530
|
+
*/
|
|
531
|
+
const createMismatchRuleDeno = (config) => ({ create: createMismatchRule(config, (context) => ({ message: actorPropertyMismatch(context) })) });
|
|
532
|
+
const createMismatchRuleEslint = (config) => ({
|
|
533
|
+
meta: {
|
|
534
|
+
type: "problem",
|
|
535
|
+
docs: { description: `Ensure actor's ${config.path.join(".")} property uses correct context method` },
|
|
536
|
+
schema: [],
|
|
537
|
+
messages: { mismatch: "{{ message }}" }
|
|
538
|
+
},
|
|
539
|
+
create: createMismatchRule(config, (context) => ({
|
|
540
|
+
messageId: "mismatch",
|
|
541
|
+
data: { message: actorPropertyMismatch(context) }
|
|
542
|
+
}))
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
//#endregion
|
|
546
|
+
//#region src/rules/actor-featured-property-mismatch.ts
|
|
547
|
+
const deno$18 = createMismatchRuleDeno(properties.featured);
|
|
548
|
+
const eslint$1 = createMismatchRuleEslint(properties.featured);
|
|
549
|
+
|
|
550
|
+
//#endregion
|
|
551
|
+
//#region src/rules/actor-featured-property-required.ts
|
|
552
|
+
const deno$17 = createRequiredRuleDeno(properties.featured);
|
|
553
|
+
const eslint$2 = createRequiredRuleEslint(properties.featured);
|
|
554
|
+
|
|
555
|
+
//#endregion
|
|
556
|
+
//#region src/rules/actor-featured-tags-property-mismatch.ts
|
|
557
|
+
const deno$16 = createMismatchRuleDeno(properties.featuredTags);
|
|
558
|
+
const eslint$3 = createMismatchRuleEslint(properties.featuredTags);
|
|
559
|
+
|
|
560
|
+
//#endregion
|
|
561
|
+
//#region src/rules/actor-featured-tags-property-required.ts
|
|
562
|
+
const deno$15 = createRequiredRuleDeno(properties.featuredTags);
|
|
563
|
+
const eslint$4 = createRequiredRuleEslint(properties.featuredTags);
|
|
564
|
+
|
|
565
|
+
//#endregion
|
|
566
|
+
//#region src/rules/actor-followers-property-mismatch.ts
|
|
567
|
+
const deno$14 = createMismatchRuleDeno(properties.followers);
|
|
568
|
+
const eslint$5 = createMismatchRuleEslint(properties.followers);
|
|
569
|
+
|
|
570
|
+
//#endregion
|
|
571
|
+
//#region src/rules/actor-followers-property-required.ts
|
|
572
|
+
const deno$13 = createRequiredRuleDeno(properties.followers);
|
|
573
|
+
const eslint$6 = createRequiredRuleEslint(properties.followers);
|
|
574
|
+
|
|
575
|
+
//#endregion
|
|
576
|
+
//#region src/rules/actor-following-property-mismatch.ts
|
|
577
|
+
const deno$12 = createMismatchRuleDeno(properties.following);
|
|
578
|
+
const eslint$7 = createMismatchRuleEslint(properties.following);
|
|
579
|
+
|
|
580
|
+
//#endregion
|
|
581
|
+
//#region src/rules/actor-following-property-required.ts
|
|
582
|
+
const deno$11 = createRequiredRuleDeno(properties.following);
|
|
583
|
+
const eslint$8 = createRequiredRuleEslint(properties.following);
|
|
584
|
+
|
|
585
|
+
//#endregion
|
|
586
|
+
//#region src/rules/actor-id-mismatch.ts
|
|
587
|
+
const deno$10 = createMismatchRuleDeno(properties.id);
|
|
588
|
+
const eslint$9 = createMismatchRuleEslint(properties.id);
|
|
589
|
+
|
|
590
|
+
//#endregion
|
|
591
|
+
//#region src/rules/actor-id-required.ts
|
|
592
|
+
const deno$9 = createRequiredRuleDeno(properties.id);
|
|
593
|
+
const eslint$10 = createRequiredRuleEslint(properties.id);
|
|
594
|
+
|
|
595
|
+
//#endregion
|
|
596
|
+
//#region src/rules/actor-inbox-property-mismatch.ts
|
|
597
|
+
const deno$8 = createMismatchRuleDeno(properties.inbox);
|
|
598
|
+
const eslint$11 = createMismatchRuleEslint(properties.inbox);
|
|
599
|
+
|
|
600
|
+
//#endregion
|
|
601
|
+
//#region src/rules/actor-inbox-property-required.ts
|
|
602
|
+
const deno$7 = createRequiredRuleDeno(properties.inbox);
|
|
603
|
+
const eslint$12 = createRequiredRuleEslint(properties.inbox);
|
|
604
|
+
|
|
605
|
+
//#endregion
|
|
606
|
+
//#region src/rules/actor-liked-property-mismatch.ts
|
|
607
|
+
const deno$6 = createMismatchRuleDeno(properties.liked);
|
|
608
|
+
const eslint$13 = createMismatchRuleEslint(properties.liked);
|
|
609
|
+
|
|
610
|
+
//#endregion
|
|
611
|
+
//#region src/rules/actor-liked-property-required.ts
|
|
612
|
+
const deno$5 = createRequiredRuleDeno(properties.liked);
|
|
613
|
+
const eslint$14 = createRequiredRuleEslint(properties.liked);
|
|
614
|
+
|
|
615
|
+
//#endregion
|
|
616
|
+
//#region src/rules/actor-outbox-property-mismatch.ts
|
|
617
|
+
const deno$4 = createMismatchRuleDeno(properties.outbox);
|
|
618
|
+
const eslint$15 = createMismatchRuleEslint(properties.outbox);
|
|
619
|
+
|
|
620
|
+
//#endregion
|
|
621
|
+
//#region src/rules/actor-outbox-property-required.ts
|
|
622
|
+
const deno$3 = createRequiredRuleDeno(properties.outbox);
|
|
623
|
+
const eslint$16 = createRequiredRuleEslint(properties.outbox);
|
|
624
|
+
|
|
625
|
+
//#endregion
|
|
626
|
+
//#region src/rules/actor-public-key-required.ts
|
|
627
|
+
const deno$2 = createRequiredRuleDeno(properties.publicKey);
|
|
628
|
+
const eslint$17 = createRequiredRuleEslint(properties.publicKey);
|
|
629
|
+
|
|
630
|
+
//#endregion
|
|
631
|
+
//#region src/rules/actor-shared-inbox-property-mismatch.ts
|
|
632
|
+
const deno$1 = createMismatchRuleDeno(properties.sharedInbox);
|
|
633
|
+
const eslint$18 = createMismatchRuleEslint(properties.sharedInbox);
|
|
634
|
+
|
|
635
|
+
//#endregion
|
|
636
|
+
//#region src/rules/actor-shared-inbox-property-required.ts
|
|
637
|
+
const deno = createRequiredRuleDeno(properties.sharedInbox);
|
|
638
|
+
const eslint$19 = createRequiredRuleEslint(properties.sharedInbox);
|
|
639
|
+
|
|
640
|
+
//#endregion
|
|
641
|
+
//#region src/rules/collection-filtering-not-implemented.ts
|
|
642
|
+
/**
|
|
643
|
+
* The followers dispatcher method that supports filtering.
|
|
644
|
+
* Only setFollowersDispatcher uses the filter parameter for
|
|
645
|
+
* followers collection synchronization.
|
|
646
|
+
* See: https://fedify.dev/manual/collections#filtering-by-server
|
|
647
|
+
*/
|
|
648
|
+
const FILTER_NEEDED = ["setFollowersDispatcher"];
|
|
649
|
+
/**
|
|
650
|
+
* Checks if a node is a setFollowersDispatcher call.
|
|
651
|
+
*/
|
|
652
|
+
const isFollowersDispatcherCall = (node) => "callee" in node && node.callee && node.callee.type === "MemberExpression" && node.callee.property.type === "Identifier" && FILTER_NEEDED.includes(node.callee.property.name);
|
|
653
|
+
/**
|
|
654
|
+
* Checks if a function node has the filter parameter (4th parameter).
|
|
655
|
+
* CollectionDispatcher signature: (context, identifier, cursor, filter?) => ...
|
|
656
|
+
*/
|
|
657
|
+
const hasFilterParameter = hasMinParams(4);
|
|
658
|
+
const eslint$20 = {
|
|
659
|
+
meta: {
|
|
660
|
+
type: "suggestion",
|
|
661
|
+
docs: { description: "Ensure followers dispatcher implements filtering" },
|
|
662
|
+
schema: [],
|
|
663
|
+
messages: { filterRequired: COLLECTION_FILTERING_NOT_IMPLEMENTED_ERROR }
|
|
664
|
+
},
|
|
665
|
+
create(context) {
|
|
666
|
+
const federationTracker = trackFederationVariables();
|
|
667
|
+
return {
|
|
668
|
+
VariableDeclarator: federationTracker.VariableDeclarator,
|
|
669
|
+
CallExpression(node) {
|
|
670
|
+
if (!isFollowersDispatcherCall(node)) return;
|
|
671
|
+
if (!federationTracker.isFederationObject(node.callee.object)) return;
|
|
672
|
+
const dispatcherArg = node.arguments[1];
|
|
673
|
+
if (!isFunction(dispatcherArg)) return;
|
|
674
|
+
if (!hasFilterParameter(dispatcherArg)) context.report({
|
|
675
|
+
node: dispatcherArg,
|
|
676
|
+
messageId: "filterRequired"
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
};
|
|
682
|
+
|
|
683
|
+
//#endregion
|
|
684
|
+
//#region src/index.ts
|
|
685
|
+
const rules = {
|
|
686
|
+
[RULE_IDS.actorIdMismatch]: eslint$9,
|
|
687
|
+
[RULE_IDS.actorIdRequired]: eslint$10,
|
|
688
|
+
[RULE_IDS.actorFollowingPropertyRequired]: eslint$8,
|
|
689
|
+
[RULE_IDS.actorFollowingPropertyMismatch]: eslint$7,
|
|
690
|
+
[RULE_IDS.actorFollowersPropertyRequired]: eslint$6,
|
|
691
|
+
[RULE_IDS.actorFollowersPropertyMismatch]: eslint$5,
|
|
692
|
+
[RULE_IDS.actorOutboxPropertyRequired]: eslint$16,
|
|
693
|
+
[RULE_IDS.actorOutboxPropertyMismatch]: eslint$15,
|
|
694
|
+
[RULE_IDS.actorLikedPropertyRequired]: eslint$14,
|
|
695
|
+
[RULE_IDS.actorLikedPropertyMismatch]: eslint$13,
|
|
696
|
+
[RULE_IDS.actorFeaturedPropertyRequired]: eslint$2,
|
|
697
|
+
[RULE_IDS.actorFeaturedPropertyMismatch]: eslint$1,
|
|
698
|
+
[RULE_IDS.actorFeaturedTagsPropertyRequired]: eslint$4,
|
|
699
|
+
[RULE_IDS.actorFeaturedTagsPropertyMismatch]: eslint$3,
|
|
700
|
+
[RULE_IDS.actorInboxPropertyRequired]: eslint$12,
|
|
701
|
+
[RULE_IDS.actorInboxPropertyMismatch]: eslint$11,
|
|
702
|
+
[RULE_IDS.actorSharedInboxPropertyRequired]: eslint$19,
|
|
703
|
+
[RULE_IDS.actorSharedInboxPropertyMismatch]: eslint$18,
|
|
704
|
+
[RULE_IDS.actorPublicKeyRequired]: eslint$17,
|
|
705
|
+
[RULE_IDS.actorAssertionMethodRequired]: eslint,
|
|
706
|
+
[RULE_IDS.collectionFilteringNotImplemented]: eslint$20
|
|
707
|
+
};
|
|
708
|
+
const recommendedRuleIds = [RULE_IDS.actorIdMismatch, RULE_IDS.actorIdRequired];
|
|
709
|
+
/**
|
|
710
|
+
* Recommended configuration - enables all rules as warnings
|
|
711
|
+
*/
|
|
712
|
+
const recommendedRules = (0, __fxts_core.pipe)(rules, __fxts_core.keys, (0, __fxts_core.map)((key) => [`@fedify/lint/${key}`, recommendedRuleIds.includes(key) ? "error" : "warn"]), __fxts_core.fromEntries);
|
|
713
|
+
/**
|
|
714
|
+
* Strict configuration - enables all rules as errors
|
|
715
|
+
*/
|
|
716
|
+
const strictRules = (0, __fxts_core.pipe)(rules, __fxts_core.keys, (0, __fxts_core.map)((key) => [`${deno_default.name}/${key}`, "error"]), __fxts_core.fromEntries);
|
|
717
|
+
const plugin = {
|
|
718
|
+
meta: {
|
|
719
|
+
name: deno_default.name,
|
|
720
|
+
version: deno_default.version
|
|
721
|
+
},
|
|
722
|
+
rules,
|
|
723
|
+
configs: {
|
|
724
|
+
recommended: {
|
|
725
|
+
plugins: [deno_default.name],
|
|
726
|
+
rules: recommendedRules
|
|
727
|
+
},
|
|
728
|
+
strict: {
|
|
729
|
+
plugins: [deno_default.name],
|
|
730
|
+
rules: strictRules
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
};
|
|
734
|
+
const recommendedConfig = {
|
|
735
|
+
files: ["federation", "federation/*"].map((filename) => [
|
|
736
|
+
filename + ".ts",
|
|
737
|
+
filename + ".tsx",
|
|
738
|
+
filename + ".js",
|
|
739
|
+
filename + ".jsx",
|
|
740
|
+
filename + ".mjs",
|
|
741
|
+
filename + ".cjs"
|
|
742
|
+
]).flat(),
|
|
743
|
+
languageOptions: { parser: __typescript_eslint_parser.default },
|
|
744
|
+
plugins: { [deno_default.name]: plugin },
|
|
745
|
+
rules: recommendedRules
|
|
746
|
+
};
|
|
747
|
+
var src_default = recommendedConfig;
|
|
748
|
+
|
|
749
|
+
//#endregion
|
|
750
|
+
exports.default = src_default;
|
|
751
|
+
exports.plugin = plugin;
|