@fedify/lint 2.4.0-dev.2030 → 2.4.0-dev.2035
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/README.md +3 -0
- package/deno.json +1 -1
- package/dist/index.cjs +28 -27
- package/dist/index.js +4 -3
- package/dist/{outbox-listener-delivery-required-BTQdF7iz.cjs → outbox-listener-delivery-required-BRGnlbDc.cjs} +620 -224
- package/dist/{outbox-listener-delivery-required-tA6wct0V.js → outbox-listener-delivery-required-Dvz7wBzC.js} +615 -225
- package/dist/oxlint.cjs +28 -27
- package/dist/oxlint.js +4 -3
- package/package.json +3 -3
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
let _fxts_core = require("@fxts/core");
|
|
2
2
|
//#region deno.json
|
|
3
3
|
var name = "@fedify/lint";
|
|
4
|
-
var version = "2.4.0-dev.
|
|
4
|
+
var version = "2.4.0-dev.2035+d1f8f8e2";
|
|
5
5
|
//#endregion
|
|
6
6
|
//#region src/lib/const.ts
|
|
7
7
|
/**
|
|
@@ -141,6 +141,7 @@ const RULE_IDS = {
|
|
|
141
141
|
actorUploadMediaPropertyMismatch: "actor-upload-media-property-mismatch",
|
|
142
142
|
collectionFilteringNotImplemented: "collection-filtering-not-implemented",
|
|
143
143
|
outboxListenerDeliveryRequired: "outbox-listener-delivery-required",
|
|
144
|
+
outboxListenerDeliveryNotAwaited: "outbox-listener-delivery-not-awaited",
|
|
144
145
|
mediaUploaderObjectUriRequired: "media-uploader-object-uri-required",
|
|
145
146
|
mediaUploaderAuthorizationRequired: "media-uploader-authorization-required"
|
|
146
147
|
};
|
|
@@ -462,7 +463,7 @@ function createRequiredRuleEslint(config) {
|
|
|
462
463
|
};
|
|
463
464
|
}
|
|
464
465
|
createRequiredRuleDeno(properties.assertionMethod);
|
|
465
|
-
const eslint$
|
|
466
|
+
const eslint$27 = createRequiredRuleEslint(properties.assertionMethod);
|
|
466
467
|
//#endregion
|
|
467
468
|
//#region src/lib/mismatch.ts
|
|
468
469
|
const isIdentifierWithName = (name) => (node) => allOf(isNodeType("Identifier"), isNodeName(name))(node);
|
|
@@ -532,49 +533,49 @@ const createMismatchRuleEslint = (config) => ({
|
|
|
532
533
|
}))
|
|
533
534
|
});
|
|
534
535
|
createMismatchRuleDeno(properties.featured);
|
|
535
|
-
const eslint$
|
|
536
|
+
const eslint$26 = createMismatchRuleEslint(properties.featured);
|
|
536
537
|
createRequiredRuleDeno(properties.featured);
|
|
537
|
-
const eslint$
|
|
538
|
+
const eslint$25 = createRequiredRuleEslint(properties.featured);
|
|
538
539
|
createMismatchRuleDeno(properties.featuredTags);
|
|
539
|
-
const eslint$
|
|
540
|
+
const eslint$24 = createMismatchRuleEslint(properties.featuredTags);
|
|
540
541
|
createRequiredRuleDeno(properties.featuredTags);
|
|
541
|
-
const eslint$
|
|
542
|
+
const eslint$23 = createRequiredRuleEslint(properties.featuredTags);
|
|
542
543
|
createMismatchRuleDeno(properties.followers);
|
|
543
|
-
const eslint$
|
|
544
|
+
const eslint$22 = createMismatchRuleEslint(properties.followers);
|
|
544
545
|
createRequiredRuleDeno(properties.followers);
|
|
545
|
-
const eslint$
|
|
546
|
+
const eslint$21 = createRequiredRuleEslint(properties.followers);
|
|
546
547
|
createMismatchRuleDeno(properties.following);
|
|
547
|
-
const eslint$
|
|
548
|
+
const eslint$20 = createMismatchRuleEslint(properties.following);
|
|
548
549
|
createRequiredRuleDeno(properties.following);
|
|
549
|
-
const eslint$
|
|
550
|
+
const eslint$19 = createRequiredRuleEslint(properties.following);
|
|
550
551
|
createMismatchRuleDeno(properties.id);
|
|
551
|
-
const eslint$
|
|
552
|
+
const eslint$18 = createMismatchRuleEslint(properties.id);
|
|
552
553
|
createRequiredRuleDeno(properties.id);
|
|
553
|
-
const eslint$
|
|
554
|
+
const eslint$17 = createRequiredRuleEslint(properties.id);
|
|
554
555
|
createMismatchRuleDeno(properties.inbox);
|
|
555
|
-
const eslint$
|
|
556
|
+
const eslint$16 = createMismatchRuleEslint(properties.inbox);
|
|
556
557
|
createRequiredRuleDeno(properties.inbox);
|
|
557
|
-
const eslint$
|
|
558
|
+
const eslint$15 = createRequiredRuleEslint(properties.inbox);
|
|
558
559
|
createMismatchRuleDeno(properties.liked);
|
|
559
|
-
const eslint$
|
|
560
|
+
const eslint$14 = createMismatchRuleEslint(properties.liked);
|
|
560
561
|
createRequiredRuleDeno(properties.liked);
|
|
561
|
-
const eslint$
|
|
562
|
+
const eslint$13 = createRequiredRuleEslint(properties.liked);
|
|
562
563
|
createMismatchRuleDeno(properties.outbox);
|
|
563
|
-
const eslint$
|
|
564
|
+
const eslint$12 = createMismatchRuleEslint(properties.outbox);
|
|
564
565
|
createRequiredRuleDeno(properties.outbox);
|
|
565
|
-
const eslint$
|
|
566
|
+
const eslint$11 = createRequiredRuleEslint(properties.outbox);
|
|
566
567
|
createRequiredRuleDeno(properties.preferredUsername);
|
|
567
|
-
const eslint$
|
|
568
|
+
const eslint$10 = createRequiredRuleEslint(properties.preferredUsername);
|
|
568
569
|
createRequiredRuleDeno(properties.publicKey);
|
|
569
|
-
const eslint$
|
|
570
|
+
const eslint$9 = createRequiredRuleEslint(properties.publicKey);
|
|
570
571
|
createMismatchRuleDeno(properties.sharedInbox);
|
|
571
|
-
const eslint$
|
|
572
|
+
const eslint$8 = createMismatchRuleEslint(properties.sharedInbox);
|
|
572
573
|
createRequiredRuleDeno(properties.sharedInbox);
|
|
573
|
-
const eslint$
|
|
574
|
+
const eslint$7 = createRequiredRuleEslint(properties.sharedInbox);
|
|
574
575
|
createMismatchRuleDeno(properties.uploadMedia);
|
|
575
|
-
const eslint$
|
|
576
|
+
const eslint$6 = createMismatchRuleEslint(properties.uploadMedia);
|
|
576
577
|
createRequiredRuleDeno(properties.uploadMedia);
|
|
577
|
-
const eslint$
|
|
578
|
+
const eslint$5 = createRequiredRuleEslint(properties.uploadMedia);
|
|
578
579
|
//#endregion
|
|
579
580
|
//#region src/rules/collection-filtering-not-implemented.ts
|
|
580
581
|
/**
|
|
@@ -593,7 +594,7 @@ const isFollowersDispatcherCall = (node) => "callee" in node && node.callee && n
|
|
|
593
594
|
* CollectionDispatcher signature: (context, identifier, cursor, filter?) => ...
|
|
594
595
|
*/
|
|
595
596
|
const hasFilterParameter = hasMinParams(4);
|
|
596
|
-
const eslint$
|
|
597
|
+
const eslint$4 = {
|
|
597
598
|
meta: {
|
|
598
599
|
type: "suggestion",
|
|
599
600
|
docs: { description: "Ensure followers dispatcher implements filtering" },
|
|
@@ -619,7 +620,7 @@ const eslint$3 = {
|
|
|
619
620
|
};
|
|
620
621
|
//#endregion
|
|
621
622
|
//#region src/rules/media-uploader-authorization-required.ts
|
|
622
|
-
const MESSAGE$
|
|
623
|
+
const MESSAGE$3 = "setMediaUploader() should be protected with .authorize(); without it the endpoint accepts uploads from anyone who can reach the URL. Add .authorize() unless a public upload endpoint is intended.";
|
|
623
624
|
function getParent(node) {
|
|
624
625
|
if (node == null || typeof node !== "object") return null;
|
|
625
626
|
const parent = node.parent;
|
|
@@ -637,7 +638,7 @@ function isDirectlyAuthorized(callNode) {
|
|
|
637
638
|
const call = getParent(member);
|
|
638
639
|
return call != null && call.type === "CallExpression" && call.callee === member;
|
|
639
640
|
}
|
|
640
|
-
function createRule$
|
|
641
|
+
function createRule$3(buildReport) {
|
|
641
642
|
return (context) => {
|
|
642
643
|
const federationTracker = trackFederationVariables();
|
|
643
644
|
const unchained = [];
|
|
@@ -675,21 +676,21 @@ function createRule$2(buildReport) {
|
|
|
675
676
|
};
|
|
676
677
|
};
|
|
677
678
|
}
|
|
678
|
-
const eslint$
|
|
679
|
+
const eslint$3 = {
|
|
679
680
|
meta: {
|
|
680
681
|
type: "suggestion",
|
|
681
682
|
docs: { description: "Warn when setMediaUploader() is registered without an authorize() hook" },
|
|
682
683
|
schema: [],
|
|
683
684
|
messages: { required: "{{ message }}" }
|
|
684
685
|
},
|
|
685
|
-
create: createRule$
|
|
686
|
+
create: createRule$3({
|
|
686
687
|
messageId: "required",
|
|
687
|
-
data: { message: MESSAGE$
|
|
688
|
+
data: { message: MESSAGE$3 }
|
|
688
689
|
})
|
|
689
690
|
};
|
|
690
691
|
//#endregion
|
|
691
692
|
//#region src/rules/media-uploader-object-uri-required.ts
|
|
692
|
-
const MESSAGE$
|
|
693
|
+
const MESSAGE$2 = "setMediaUploader() callbacks should return a value derived from ctx.getObjectUri(): either return ctx.getObjectUri(...) directly, or an object whose id is ctx.getObjectUri(...).";
|
|
693
694
|
const GETTER_NAME = "getObjectUri";
|
|
694
695
|
const getMemberPropertyName$1 = (node) => {
|
|
695
696
|
if (node.type !== "MemberExpression") return null;
|
|
@@ -963,7 +964,7 @@ function callbackDerivesFromGetObjectUri(callback) {
|
|
|
963
964
|
};
|
|
964
965
|
return returnedExpressions.every((expr) => returnDerivesFromGetObjectUri(expr, ctx, /* @__PURE__ */ new Set()));
|
|
965
966
|
}
|
|
966
|
-
function createRule$
|
|
967
|
+
function createRule$2(buildReport) {
|
|
967
968
|
return (context) => {
|
|
968
969
|
const federationTracker = trackFederationVariables();
|
|
969
970
|
const bindings = /* @__PURE__ */ new Map();
|
|
@@ -997,21 +998,21 @@ function createRule$1(buildReport) {
|
|
|
997
998
|
};
|
|
998
999
|
};
|
|
999
1000
|
}
|
|
1000
|
-
const eslint$
|
|
1001
|
+
const eslint$2 = {
|
|
1001
1002
|
meta: {
|
|
1002
1003
|
type: "suggestion",
|
|
1003
1004
|
docs: { description: "Warn when a setMediaUploader() callback does not derive its return value from ctx.getObjectUri()" },
|
|
1004
1005
|
schema: [],
|
|
1005
1006
|
messages: { required: "{{ message }}" }
|
|
1006
1007
|
},
|
|
1007
|
-
create: createRule$
|
|
1008
|
+
create: createRule$2({
|
|
1008
1009
|
messageId: "required",
|
|
1009
|
-
data: { message: MESSAGE$
|
|
1010
|
+
data: { message: MESSAGE$2 }
|
|
1010
1011
|
})
|
|
1011
1012
|
};
|
|
1012
1013
|
//#endregion
|
|
1013
|
-
//#region src/
|
|
1014
|
-
const
|
|
1014
|
+
//#region src/lib/outbox-listener.ts
|
|
1015
|
+
const DELIVERY_METHOD_NAMES = /* @__PURE__ */ new Set(["sendActivity", "forwardActivity"]);
|
|
1015
1016
|
const isChainedFromOutboxListeners = (expr, federationTracker) => {
|
|
1016
1017
|
if (expr.type !== "CallExpression") return false;
|
|
1017
1018
|
if (!hasMemberExpressionCallee(expr) || !hasIdentifierProperty(expr)) return false;
|
|
@@ -1020,13 +1021,6 @@ const isChainedFromOutboxListeners = (expr, federationTracker) => {
|
|
|
1020
1021
|
if (methodName === "authorize" || methodName === "onError" || methodName === "on") return isChainedFromOutboxListeners(expr.callee.object, federationTracker);
|
|
1021
1022
|
return false;
|
|
1022
1023
|
};
|
|
1023
|
-
const DELIVERY_METHOD_NAMES = /* @__PURE__ */ new Set(["sendActivity", "forwardActivity"]);
|
|
1024
|
-
const FUNCTION_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
1025
|
-
"FunctionDeclaration",
|
|
1026
|
-
"FunctionExpression",
|
|
1027
|
-
"ArrowFunctionExpression"
|
|
1028
|
-
]);
|
|
1029
|
-
const isFunctionLikeNode = (node) => FUNCTION_NODE_TYPES.has(node.type);
|
|
1030
1024
|
const getMemberPropertyName = (expr) => {
|
|
1031
1025
|
if (expr.type !== "MemberExpression") return null;
|
|
1032
1026
|
const property = expr.property;
|
|
@@ -1039,133 +1033,6 @@ function unwrapContextParam(node) {
|
|
|
1039
1033
|
while (current?.type === "AssignmentPattern") current = current.left;
|
|
1040
1034
|
return current;
|
|
1041
1035
|
}
|
|
1042
|
-
function escapeRegExp(value) {
|
|
1043
|
-
return value.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1044
|
-
}
|
|
1045
|
-
function stripCommentsAndStrings(code) {
|
|
1046
|
-
let result = "";
|
|
1047
|
-
let index = 0;
|
|
1048
|
-
const skipQuotedString = (quote) => {
|
|
1049
|
-
const start = index;
|
|
1050
|
-
index += 1;
|
|
1051
|
-
while (index < code.length) {
|
|
1052
|
-
const char = code[index];
|
|
1053
|
-
if (char === "\\") {
|
|
1054
|
-
index += 2;
|
|
1055
|
-
continue;
|
|
1056
|
-
}
|
|
1057
|
-
index += 1;
|
|
1058
|
-
if (char === quote) break;
|
|
1059
|
-
}
|
|
1060
|
-
const literal = code.slice(start, index);
|
|
1061
|
-
const value = literal.slice(1, -1);
|
|
1062
|
-
result += DELIVERY_METHOD_NAMES.has(value) ? literal : `${quote}${quote}`;
|
|
1063
|
-
};
|
|
1064
|
-
const stripTemplateLiteral = () => {
|
|
1065
|
-
const start = index;
|
|
1066
|
-
index += 1;
|
|
1067
|
-
let raw = "";
|
|
1068
|
-
let hasExpression = false;
|
|
1069
|
-
while (index < code.length) {
|
|
1070
|
-
const char = code[index];
|
|
1071
|
-
if (char === "\\") {
|
|
1072
|
-
raw += char;
|
|
1073
|
-
raw += code[index + 1] ?? "";
|
|
1074
|
-
index += 2;
|
|
1075
|
-
continue;
|
|
1076
|
-
}
|
|
1077
|
-
if (char === "`") {
|
|
1078
|
-
index += 1;
|
|
1079
|
-
if (!hasExpression && DELIVERY_METHOD_NAMES.has(raw)) result += code.slice(start, index);
|
|
1080
|
-
else result += "``";
|
|
1081
|
-
return;
|
|
1082
|
-
}
|
|
1083
|
-
if (char === "$" && code[index + 1] === "{") {
|
|
1084
|
-
hasExpression = true;
|
|
1085
|
-
result += "`${";
|
|
1086
|
-
index += 2;
|
|
1087
|
-
let depth = 1;
|
|
1088
|
-
while (index < code.length && depth > 0) {
|
|
1089
|
-
const exprChar = code[index];
|
|
1090
|
-
const next = code[index + 1];
|
|
1091
|
-
if (exprChar === "'" || exprChar === "\"") {
|
|
1092
|
-
skipQuotedString(exprChar);
|
|
1093
|
-
continue;
|
|
1094
|
-
}
|
|
1095
|
-
if (exprChar === "`") {
|
|
1096
|
-
stripTemplateLiteral();
|
|
1097
|
-
continue;
|
|
1098
|
-
}
|
|
1099
|
-
if (exprChar === "/" && next === "*") {
|
|
1100
|
-
index += 2;
|
|
1101
|
-
while (index < code.length) {
|
|
1102
|
-
if (code[index] === "*" && code[index + 1] === "/") {
|
|
1103
|
-
index += 2;
|
|
1104
|
-
break;
|
|
1105
|
-
}
|
|
1106
|
-
index += 1;
|
|
1107
|
-
}
|
|
1108
|
-
continue;
|
|
1109
|
-
}
|
|
1110
|
-
if (exprChar === "/" && next === "/") {
|
|
1111
|
-
index += 2;
|
|
1112
|
-
while (index < code.length && code[index] !== "\n") index += 1;
|
|
1113
|
-
continue;
|
|
1114
|
-
}
|
|
1115
|
-
result += exprChar;
|
|
1116
|
-
index += 1;
|
|
1117
|
-
if (exprChar === "{") depth += 1;
|
|
1118
|
-
else if (exprChar === "}") depth -= 1;
|
|
1119
|
-
}
|
|
1120
|
-
continue;
|
|
1121
|
-
}
|
|
1122
|
-
raw += char;
|
|
1123
|
-
index += 1;
|
|
1124
|
-
}
|
|
1125
|
-
result += "``";
|
|
1126
|
-
};
|
|
1127
|
-
while (index < code.length) {
|
|
1128
|
-
const char = code[index];
|
|
1129
|
-
const next = code[index + 1];
|
|
1130
|
-
if (char === "/" && next === "*") {
|
|
1131
|
-
index += 2;
|
|
1132
|
-
while (index < code.length) {
|
|
1133
|
-
if (code[index] === "*" && code[index + 1] === "/") {
|
|
1134
|
-
index += 2;
|
|
1135
|
-
break;
|
|
1136
|
-
}
|
|
1137
|
-
index += 1;
|
|
1138
|
-
}
|
|
1139
|
-
continue;
|
|
1140
|
-
}
|
|
1141
|
-
if (char === "/" && next === "/") {
|
|
1142
|
-
index += 2;
|
|
1143
|
-
while (index < code.length && code[index] !== "\n") index += 1;
|
|
1144
|
-
continue;
|
|
1145
|
-
}
|
|
1146
|
-
if (char === "'" || char === "\"") {
|
|
1147
|
-
skipQuotedString(char);
|
|
1148
|
-
continue;
|
|
1149
|
-
}
|
|
1150
|
-
if (char === "`") {
|
|
1151
|
-
stripTemplateLiteral();
|
|
1152
|
-
continue;
|
|
1153
|
-
}
|
|
1154
|
-
result += char;
|
|
1155
|
-
index += 1;
|
|
1156
|
-
}
|
|
1157
|
-
return result;
|
|
1158
|
-
}
|
|
1159
|
-
function getDeliveryAliasName(node) {
|
|
1160
|
-
if (node.type === "Identifier") return node.name;
|
|
1161
|
-
if (node.type === "AssignmentPattern" && node.left.type === "Identifier") return node.left.name;
|
|
1162
|
-
return null;
|
|
1163
|
-
}
|
|
1164
|
-
function buildContextExpressionPattern(contextName) {
|
|
1165
|
-
const name = escapeRegExp(contextName);
|
|
1166
|
-
const boundedName = String.raw`(?<![\w$])${name}(?![\w$])`;
|
|
1167
|
-
return String.raw`(?:${boundedName}|\(\s*${boundedName}(?:\s+as\s+[^)]+)?\s*\))`;
|
|
1168
|
-
}
|
|
1169
1036
|
/**
|
|
1170
1037
|
* Resolves an expression to the function it refers to: a direct function
|
|
1171
1038
|
* literal, a local variable bound to one, or a property of a local object
|
|
@@ -1198,6 +1065,49 @@ const resolveFunctionBinding = (expr, bindings, seen = /* @__PURE__ */ new Set()
|
|
|
1198
1065
|
}
|
|
1199
1066
|
return null;
|
|
1200
1067
|
};
|
|
1068
|
+
/**
|
|
1069
|
+
* Builds the visitor an outbox-listener rule registers: it follows
|
|
1070
|
+
* `federation.setOutboxListeners(...).on(Type, listener)` chains through the
|
|
1071
|
+
* file and, once the whole file has been seen, hands each listener, resolved
|
|
1072
|
+
* to its function, to `onListener`. The listener may be an inline function or
|
|
1073
|
+
* a name bound to one elsewhere in the file.
|
|
1074
|
+
*/
|
|
1075
|
+
function createOutboxListenerVisitor(onListener) {
|
|
1076
|
+
const federationTracker = trackFederationVariables();
|
|
1077
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
1078
|
+
const pendingCalls = [];
|
|
1079
|
+
const inspectCall = (node) => {
|
|
1080
|
+
if (!hasMemberExpressionCallee(node) || !hasIdentifierProperty(node) || !hasMethodName("on")(node) || node.arguments.length < 2) return;
|
|
1081
|
+
if (!isChainedFromOutboxListeners(node.callee.object, federationTracker)) return;
|
|
1082
|
+
const listener = node.arguments[1];
|
|
1083
|
+
const resolvedListener = isNode(listener) && isFunction(listener) ? listener : isNode(listener) ? resolveFunctionBinding(listener, bindings) : null;
|
|
1084
|
+
if (resolvedListener == null) return;
|
|
1085
|
+
onListener(resolvedListener);
|
|
1086
|
+
};
|
|
1087
|
+
return {
|
|
1088
|
+
VariableDeclarator(node) {
|
|
1089
|
+
federationTracker.VariableDeclarator(node);
|
|
1090
|
+
if (node.id.type === "Identifier" && node.init != null) bindings.set(node.id.name, node.init);
|
|
1091
|
+
},
|
|
1092
|
+
FunctionDeclaration(node) {
|
|
1093
|
+
if (node.id != null) bindings.set(node.id.name, node);
|
|
1094
|
+
},
|
|
1095
|
+
CallExpression(node) {
|
|
1096
|
+
pendingCalls.push(node);
|
|
1097
|
+
},
|
|
1098
|
+
"Program:exit"() {
|
|
1099
|
+
for (const node of pendingCalls) inspectCall(node);
|
|
1100
|
+
}
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
//#endregion
|
|
1104
|
+
//#region src/lib/reachability.ts
|
|
1105
|
+
const FUNCTION_NODE_TYPES = /* @__PURE__ */ new Set([
|
|
1106
|
+
"FunctionDeclaration",
|
|
1107
|
+
"FunctionExpression",
|
|
1108
|
+
"ArrowFunctionExpression"
|
|
1109
|
+
]);
|
|
1110
|
+
const isFunctionLikeNode = (node) => FUNCTION_NODE_TYPES.has(node.type);
|
|
1201
1111
|
const isStaticallyFalsy = (test) => test.type === "Literal" && !test.value;
|
|
1202
1112
|
const isStaticallyTruthy = (test) => test.type === "Literal" && Boolean(test.value);
|
|
1203
1113
|
/**
|
|
@@ -1297,10 +1207,10 @@ function collectReachableStatements(node, out) {
|
|
|
1297
1207
|
* `deliver`), and the target of an assignment, which writes to a name
|
|
1298
1208
|
* instead of reading it.
|
|
1299
1209
|
*/
|
|
1300
|
-
function collectReferencedNames(node, out) {
|
|
1210
|
+
function collectReferencedNames(node, out, crossFunctions = false) {
|
|
1301
1211
|
if (node == null || typeof node !== "object") return;
|
|
1302
1212
|
if (Array.isArray(node)) {
|
|
1303
|
-
for (const item of node) collectReferencedNames(item, out);
|
|
1213
|
+
for (const item of node) collectReferencedNames(item, out, crossFunctions);
|
|
1304
1214
|
return;
|
|
1305
1215
|
}
|
|
1306
1216
|
if (!isNode(node)) return;
|
|
@@ -1310,37 +1220,40 @@ function collectReferencedNames(node, out) {
|
|
|
1310
1220
|
return;
|
|
1311
1221
|
}
|
|
1312
1222
|
if (n.type === "MemberExpression" && !n.computed) {
|
|
1313
|
-
collectReferencedNames(n.object, out);
|
|
1223
|
+
collectReferencedNames(n.object, out, crossFunctions);
|
|
1314
1224
|
return;
|
|
1315
1225
|
}
|
|
1316
1226
|
if (n.type === "Property" && !n.computed) {
|
|
1317
|
-
collectReferencedNames(n.value, out);
|
|
1227
|
+
collectReferencedNames(n.value, out, crossFunctions);
|
|
1318
1228
|
return;
|
|
1319
1229
|
}
|
|
1320
1230
|
if (n.type === "VariableDeclarator") {
|
|
1321
|
-
if (n.init != null) collectReferencedNames(n.init, out);
|
|
1231
|
+
if (n.init != null) collectReferencedNames(n.init, out, crossFunctions);
|
|
1322
1232
|
return;
|
|
1323
1233
|
}
|
|
1324
1234
|
if (n.type === "ClassDeclaration" || n.type === "ClassExpression") {
|
|
1325
|
-
collectReferencedNames(n.superClass, out);
|
|
1326
|
-
collectReferencedNames(n.body, out);
|
|
1235
|
+
collectReferencedNames(n.superClass, out, crossFunctions);
|
|
1236
|
+
collectReferencedNames(n.body, out, crossFunctions);
|
|
1327
1237
|
return;
|
|
1328
1238
|
}
|
|
1329
1239
|
if ((n.type === "MethodDefinition" || n.type === "PropertyDefinition") && !n.computed) {
|
|
1330
|
-
collectReferencedNames(n.value, out);
|
|
1240
|
+
collectReferencedNames(n.value, out, crossFunctions);
|
|
1331
1241
|
return;
|
|
1332
1242
|
}
|
|
1333
1243
|
if (n.type === "AssignmentExpression" && n.operator === "=") {
|
|
1334
1244
|
if (getAssignmentTargetName(n.left) != null) {
|
|
1335
|
-
collectReferencedNames(n.right, out);
|
|
1245
|
+
collectReferencedNames(n.right, out, crossFunctions);
|
|
1336
1246
|
return;
|
|
1337
1247
|
}
|
|
1338
1248
|
}
|
|
1339
|
-
if (isFunctionLikeNode(n))
|
|
1249
|
+
if (isFunctionLikeNode(n)) {
|
|
1250
|
+
if (crossFunctions) collectReferencedNames(n.body, out, crossFunctions);
|
|
1251
|
+
return;
|
|
1252
|
+
}
|
|
1340
1253
|
const record = n;
|
|
1341
1254
|
for (const key in record) {
|
|
1342
1255
|
if (key === "parent") continue;
|
|
1343
|
-
collectReferencedNames(record[key], out);
|
|
1256
|
+
collectReferencedNames(record[key], out, crossFunctions);
|
|
1344
1257
|
}
|
|
1345
1258
|
}
|
|
1346
1259
|
/**
|
|
@@ -1480,19 +1393,20 @@ function collectNestedFunctions(node, out) {
|
|
|
1480
1393
|
}
|
|
1481
1394
|
}
|
|
1482
1395
|
/**
|
|
1483
|
-
*
|
|
1484
|
-
*
|
|
1485
|
-
*
|
|
1486
|
-
*
|
|
1487
|
-
* function's body -- gets queued in
|
|
1488
|
-
* fresh for each scope, so a name
|
|
1489
|
-
* same-named one further out instead of
|
|
1490
|
-
* dead branch that merely mentions a name
|
|
1396
|
+
* Walks every scope that is actually reachable from `root`, calling `visit`
|
|
1397
|
+
* for each, and returns the set of function nodes found to be used: `root`
|
|
1398
|
+
* itself feeds a worklist, and each function it (or a function already on
|
|
1399
|
+
* the worklist) uses from *its own* reachable statements -- never from a
|
|
1400
|
+
* dead branch or some other not-yet-reached function's body -- gets queued in
|
|
1401
|
+
* turn. `outerFunctionsByName` is layered fresh for each scope, so a name
|
|
1402
|
+
* bound at an inner scope shadows a same-named one further out instead of
|
|
1403
|
+
* overwriting it globally, and a dead branch that merely mentions a name
|
|
1404
|
+
* never queues what it holds.
|
|
1491
1405
|
*/
|
|
1492
|
-
function
|
|
1406
|
+
function walkUsedScopes(root, visit) {
|
|
1493
1407
|
const used = /* @__PURE__ */ new Set();
|
|
1494
1408
|
const visited = /* @__PURE__ */ new Set();
|
|
1495
|
-
const processScope = (scopeRoot, outerFunctionsByName) => {
|
|
1409
|
+
const processScope = (scopeRoot, scopeFn, outerFunctionsByName) => {
|
|
1496
1410
|
if (visited.has(scopeRoot)) return;
|
|
1497
1411
|
visited.add(scopeRoot);
|
|
1498
1412
|
const statements = [];
|
|
@@ -1501,6 +1415,11 @@ function computeUsedFunctions(root) {
|
|
|
1501
1415
|
for (const statement of statements) collectFunctionsByName(statement, functionsHere);
|
|
1502
1416
|
const functionsByName = new Map(outerFunctionsByName);
|
|
1503
1417
|
for (const [name, functions] of functionsHere) functionsByName.set(name, functions);
|
|
1418
|
+
visit({
|
|
1419
|
+
fn: scopeFn,
|
|
1420
|
+
statements,
|
|
1421
|
+
functionsByName
|
|
1422
|
+
});
|
|
1504
1423
|
const referencedNames = /* @__PURE__ */ new Set();
|
|
1505
1424
|
for (const statement of statements) collectReferencedNames(statement, referencedNames);
|
|
1506
1425
|
const reached = /* @__PURE__ */ new Set();
|
|
@@ -1515,15 +1434,22 @@ function computeUsedFunctions(root) {
|
|
|
1515
1434
|
collectNestedFunctions(statement, nested);
|
|
1516
1435
|
for (const fn of nested) if (!held.has(fn)) reached.add(fn);
|
|
1517
1436
|
}
|
|
1518
|
-
for (const
|
|
1519
|
-
used.add(
|
|
1520
|
-
processScope(
|
|
1437
|
+
for (const reachedFn of reached) {
|
|
1438
|
+
used.add(reachedFn);
|
|
1439
|
+
processScope(reachedFn.body, reachedFn, functionsByName);
|
|
1521
1440
|
}
|
|
1522
1441
|
};
|
|
1523
|
-
processScope(root, /* @__PURE__ */ new Map());
|
|
1442
|
+
processScope(root, null, /* @__PURE__ */ new Map());
|
|
1524
1443
|
return used;
|
|
1525
1444
|
}
|
|
1526
1445
|
/**
|
|
1446
|
+
* The set of function nodes that are actually reachable from `root`. See
|
|
1447
|
+
* `walkUsedScopes`.
|
|
1448
|
+
*/
|
|
1449
|
+
function computeUsedFunctions(root) {
|
|
1450
|
+
return walkUsedScopes(root, () => {});
|
|
1451
|
+
}
|
|
1452
|
+
/**
|
|
1527
1453
|
* A node's `[start, end)` character offsets into the whole source file.
|
|
1528
1454
|
* Both engines always populate this -- ESLint forces it on regardless of
|
|
1529
1455
|
* parser options, and Deno.lint exposes it the same way as every other
|
|
@@ -1533,6 +1459,493 @@ function computeUsedFunctions(root) {
|
|
|
1533
1459
|
function getRange(node) {
|
|
1534
1460
|
return node.range;
|
|
1535
1461
|
}
|
|
1462
|
+
//#endregion
|
|
1463
|
+
//#region src/rules/outbox-listener-delivery-not-awaited.ts
|
|
1464
|
+
const MESSAGE$1 = "Delivery is not awaited, so the activity may be lost once the handler returns (for example on Cloudflare Workers). Await it, return it, or pass it to waitUntil().";
|
|
1465
|
+
const HANDLED = {
|
|
1466
|
+
fate: "handled",
|
|
1467
|
+
owner: null
|
|
1468
|
+
};
|
|
1469
|
+
const DROPPED = {
|
|
1470
|
+
fate: "dropped",
|
|
1471
|
+
owner: null
|
|
1472
|
+
};
|
|
1473
|
+
/** Wrappers that pass a value through unchanged. */
|
|
1474
|
+
const TRANSPARENT_WRAPPERS = /* @__PURE__ */ new Set([
|
|
1475
|
+
"ChainExpression",
|
|
1476
|
+
"TSAsExpression",
|
|
1477
|
+
"TSNonNullExpression",
|
|
1478
|
+
"TSSatisfiesExpression",
|
|
1479
|
+
"TSTypeAssertion",
|
|
1480
|
+
"ParenthesizedExpression"
|
|
1481
|
+
]);
|
|
1482
|
+
/**
|
|
1483
|
+
* `Promise` methods that turn promises into one promise. `race()` and
|
|
1484
|
+
* `any()` settle as soon as one input does, which is accepted as a
|
|
1485
|
+
* deliberate choice to stop waiting, like `void`. A `race()` or `any()`
|
|
1486
|
+
* that is itself dropped is still reported.
|
|
1487
|
+
*/
|
|
1488
|
+
const PROMISE_COMBINATORS = /* @__PURE__ */ new Set([
|
|
1489
|
+
"all",
|
|
1490
|
+
"allSettled",
|
|
1491
|
+
"race",
|
|
1492
|
+
"any"
|
|
1493
|
+
]);
|
|
1494
|
+
/** Methods on a promise that return a new promise carrying the chain on. */
|
|
1495
|
+
const PROMISE_CHAIN_METHODS = /* @__PURE__ */ new Set([
|
|
1496
|
+
"then",
|
|
1497
|
+
"catch",
|
|
1498
|
+
"finally"
|
|
1499
|
+
]);
|
|
1500
|
+
const get = (node, key) => node == null ? void 0 : node[key];
|
|
1501
|
+
const asNode = (value) => isNode(value) ? value : null;
|
|
1502
|
+
const parentOf = (node) => asNode(get(node, "parent"));
|
|
1503
|
+
function unwrap(node) {
|
|
1504
|
+
let current = node;
|
|
1505
|
+
while (TRANSPARENT_WRAPPERS.has(current.type)) {
|
|
1506
|
+
const inner = asNode(get(current, "expression"));
|
|
1507
|
+
if (inner == null) break;
|
|
1508
|
+
current = inner;
|
|
1509
|
+
}
|
|
1510
|
+
return current;
|
|
1511
|
+
}
|
|
1512
|
+
/** The name a member expression accesses, when it is spelled out plainly. */
|
|
1513
|
+
function memberName(node) {
|
|
1514
|
+
if (node.type !== "MemberExpression") return null;
|
|
1515
|
+
const property = asNode(get(node, "property"));
|
|
1516
|
+
if (property == null) return null;
|
|
1517
|
+
if (get(node, "computed") !== true) return property.type === "Identifier" ? property.name : null;
|
|
1518
|
+
if (property.type === "Literal" && typeof property.value === "string") return property.value;
|
|
1519
|
+
if (property.type === "TemplateLiteral") {
|
|
1520
|
+
const quasis = get(property, "quasis");
|
|
1521
|
+
if (get(property, "expressions").length === 0 && quasis.length === 1) return quasis[0].cooked ?? quasis[0].value?.cooked ?? null;
|
|
1522
|
+
}
|
|
1523
|
+
return null;
|
|
1524
|
+
}
|
|
1525
|
+
/** Visits every node in `node`, stopping at a nested function unless asked to cross it. */
|
|
1526
|
+
function visitAll(node, visitor, crossFunctions = true) {
|
|
1527
|
+
if (node == null || typeof node !== "object") return;
|
|
1528
|
+
if (Array.isArray(node)) {
|
|
1529
|
+
for (const item of node) visitAll(item, visitor, crossFunctions);
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
if (!isNode(node)) return;
|
|
1533
|
+
if (!crossFunctions && isFunctionLikeNode(node)) return;
|
|
1534
|
+
visitor(node);
|
|
1535
|
+
const record = node;
|
|
1536
|
+
for (const key in record) if (key !== "parent") visitAll(record[key], visitor, crossFunctions);
|
|
1537
|
+
}
|
|
1538
|
+
/** Collects the calls in `node` that run in the same function, not in one nested in it. */
|
|
1539
|
+
function collectCalls(node, out) {
|
|
1540
|
+
visitAll(node, (n) => {
|
|
1541
|
+
if (n.type === "CallExpression") out.push(n);
|
|
1542
|
+
}, false);
|
|
1543
|
+
}
|
|
1544
|
+
function enclosingFunction(node) {
|
|
1545
|
+
for (let current = parentOf(node); current != null;) {
|
|
1546
|
+
if (isFunctionLikeNode(current)) return current;
|
|
1547
|
+
current = parentOf(current);
|
|
1548
|
+
}
|
|
1549
|
+
return null;
|
|
1550
|
+
}
|
|
1551
|
+
/** The names an object pattern binds the delivery methods to. */
|
|
1552
|
+
function deliveryAliasesOf(pattern, out) {
|
|
1553
|
+
if (pattern.type !== "ObjectPattern") return;
|
|
1554
|
+
for (const prop of get(pattern, "properties")) {
|
|
1555
|
+
if (prop.type !== "Property") continue;
|
|
1556
|
+
const key = asNode(get(prop, "key"));
|
|
1557
|
+
const keyName = key?.type === "Identifier" ? key.name : key?.type === "Literal" && typeof key.value === "string" ? key.value : null;
|
|
1558
|
+
if (keyName == null || !DELIVERY_METHOD_NAMES.has(keyName)) continue;
|
|
1559
|
+
const value = asNode(get(prop, "value"));
|
|
1560
|
+
if (value?.type === "Identifier") out.add(value.name);
|
|
1561
|
+
else if (value?.type === "AssignmentPattern") {
|
|
1562
|
+
const left = asNode(get(value, "left"));
|
|
1563
|
+
if (left?.type === "Identifier") out.add(left.name);
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
}
|
|
1567
|
+
/**
|
|
1568
|
+
* Works out how the listener refers to the delivery methods: through its
|
|
1569
|
+
* context parameter (`ctx.sendActivity`), or through a name bound to one of
|
|
1570
|
+
* them, either in the parameter (`{ sendActivity }`) or in code that can run
|
|
1571
|
+
* (`const { sendActivity } = ctx`, `const send = ctx.sendActivity`). A name
|
|
1572
|
+
* bound in a helper nothing uses, or in a dead branch, does not count.
|
|
1573
|
+
*/
|
|
1574
|
+
function findDeliveryTargets(listener, scopes) {
|
|
1575
|
+
const aliases = /* @__PURE__ */ new Set();
|
|
1576
|
+
const contextParam = unwrapContextParam(listener.params[0]);
|
|
1577
|
+
const contextName = contextParam?.type === "Identifier" ? contextParam.name : null;
|
|
1578
|
+
if (contextParam != null) deliveryAliasesOf(contextParam, aliases);
|
|
1579
|
+
if (contextName != null) for (const scope of scopes) for (const statement of scope.statements) visitAll(statement, (node) => {
|
|
1580
|
+
if (node.type !== "VariableDeclarator") return;
|
|
1581
|
+
const id = asNode(get(node, "id"));
|
|
1582
|
+
const init = asNode(get(node, "init"));
|
|
1583
|
+
if (id == null || init == null) return;
|
|
1584
|
+
const source = unwrap(init);
|
|
1585
|
+
if (source.type === "Identifier" && source.name === contextName) {
|
|
1586
|
+
deliveryAliasesOf(id, aliases);
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
if (id.type !== "Identifier" || source.type !== "MemberExpression") return;
|
|
1590
|
+
const object = unwrap(asNode(get(source, "object")) ?? source);
|
|
1591
|
+
const name = memberName(source);
|
|
1592
|
+
if (object.type === "Identifier" && object.name === contextName && name != null && DELIVERY_METHOD_NAMES.has(name)) aliases.add(id.name);
|
|
1593
|
+
}, false);
|
|
1594
|
+
return {
|
|
1595
|
+
contextName,
|
|
1596
|
+
aliases
|
|
1597
|
+
};
|
|
1598
|
+
}
|
|
1599
|
+
/**
|
|
1600
|
+
* Where the value a callback returns goes, judged by the call that receives
|
|
1601
|
+
* the callback.
|
|
1602
|
+
*/
|
|
1603
|
+
function callbackResultOutcome(call, analysis) {
|
|
1604
|
+
const name = memberName(unwrap(asNode(get(call, "callee")) ?? call));
|
|
1605
|
+
if (name === "forEach") return DROPPED;
|
|
1606
|
+
if (name === "map" || name === "flatMap") return fateOf(call, analysis, "promises");
|
|
1607
|
+
if (name != null && PROMISE_CHAIN_METHODS.has(name)) return fateOf(call, analysis, "promise");
|
|
1608
|
+
return HANDLED;
|
|
1609
|
+
}
|
|
1610
|
+
/** Where the promise a function returns goes, given who calls the function. */
|
|
1611
|
+
function returnedOutcome(fn, analysis) {
|
|
1612
|
+
if (fn == null) return HANDLED;
|
|
1613
|
+
if (fn === analysis.listener) return {
|
|
1614
|
+
fate: "returned",
|
|
1615
|
+
owner: fn
|
|
1616
|
+
};
|
|
1617
|
+
const parent = parentOf(fn);
|
|
1618
|
+
if (parent?.type === "CallExpression") {
|
|
1619
|
+
if (get(parent, "callee") === fn) return fateOf(parent, analysis, "promise");
|
|
1620
|
+
if (get(parent, "arguments").includes(fn)) return callbackResultOutcome(parent, analysis);
|
|
1621
|
+
}
|
|
1622
|
+
return {
|
|
1623
|
+
fate: "returned",
|
|
1624
|
+
owner: fn
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
/**
|
|
1628
|
+
* Where an array of promises, or an object holding one, goes when it is
|
|
1629
|
+
* returned. The listener's caller does not wait for what is inside it. Any
|
|
1630
|
+
* other function hands it to its own callers, who may well pass it to
|
|
1631
|
+
* `Promise.all()` or read the promise back out of it.
|
|
1632
|
+
*/
|
|
1633
|
+
function returnedContainerOutcome(fn, analysis) {
|
|
1634
|
+
return fn === analysis.listener ? DROPPED : HANDLED;
|
|
1635
|
+
}
|
|
1636
|
+
/** Follows the value of `expression` up through its parents to where it ends up. */
|
|
1637
|
+
function fateOf(expression, analysis, shape = "promise") {
|
|
1638
|
+
let current = expression;
|
|
1639
|
+
let currentShape = shape;
|
|
1640
|
+
for (;;) {
|
|
1641
|
+
const parent = parentOf(current);
|
|
1642
|
+
if (parent == null) return HANDLED;
|
|
1643
|
+
if (TRANSPARENT_WRAPPERS.has(parent.type)) {
|
|
1644
|
+
current = parent;
|
|
1645
|
+
continue;
|
|
1646
|
+
}
|
|
1647
|
+
switch (parent.type) {
|
|
1648
|
+
case "AwaitExpression": return currentShape === "promise" ? {
|
|
1649
|
+
fate: "awaited",
|
|
1650
|
+
owner: enclosingFunction(parent)
|
|
1651
|
+
} : DROPPED;
|
|
1652
|
+
case "ReturnStatement": return currentShape === "promise" ? returnedOutcome(enclosingFunction(parent), analysis) : returnedContainerOutcome(enclosingFunction(parent), analysis);
|
|
1653
|
+
case "ArrowFunctionExpression":
|
|
1654
|
+
if (get(parent, "body") !== current) return HANDLED;
|
|
1655
|
+
return currentShape === "promise" ? returnedOutcome(parent, analysis) : returnedContainerOutcome(parent, analysis);
|
|
1656
|
+
case "ExpressionStatement": return DROPPED;
|
|
1657
|
+
case "UnaryExpression": return get(parent, "operator") === "void" ? HANDLED : DROPPED;
|
|
1658
|
+
case "SequenceExpression": {
|
|
1659
|
+
const expressions = get(parent, "expressions");
|
|
1660
|
+
if (expressions[expressions.length - 1] !== current) return DROPPED;
|
|
1661
|
+
current = parent;
|
|
1662
|
+
continue;
|
|
1663
|
+
}
|
|
1664
|
+
case "ConditionalExpression":
|
|
1665
|
+
if (get(parent, "test") === current) return DROPPED;
|
|
1666
|
+
current = parent;
|
|
1667
|
+
continue;
|
|
1668
|
+
case "IfStatement":
|
|
1669
|
+
case "WhileStatement":
|
|
1670
|
+
case "DoWhileStatement":
|
|
1671
|
+
case "ForStatement": return DROPPED;
|
|
1672
|
+
case "LogicalExpression":
|
|
1673
|
+
case "Property":
|
|
1674
|
+
current = parent;
|
|
1675
|
+
continue;
|
|
1676
|
+
case "ObjectExpression":
|
|
1677
|
+
if (currentShape === "promise") currentShape = "object";
|
|
1678
|
+
current = parent;
|
|
1679
|
+
continue;
|
|
1680
|
+
case "ArrayExpression":
|
|
1681
|
+
if (currentShape !== "promise") return HANDLED;
|
|
1682
|
+
currentShape = "promises";
|
|
1683
|
+
current = parent;
|
|
1684
|
+
continue;
|
|
1685
|
+
case "SpreadElement": {
|
|
1686
|
+
const array = parentOf(parent);
|
|
1687
|
+
if (currentShape !== "promises" || array?.type !== "ArrayExpression") return HANDLED;
|
|
1688
|
+
current = array;
|
|
1689
|
+
continue;
|
|
1690
|
+
}
|
|
1691
|
+
case "MemberExpression": {
|
|
1692
|
+
if (get(parent, "object") !== current) return HANDLED;
|
|
1693
|
+
const call = parentOf(parent);
|
|
1694
|
+
const name = memberName(parent);
|
|
1695
|
+
if (currentShape === "promise" && call?.type === "CallExpression" && get(call, "callee") === parent && name != null && PROMISE_CHAIN_METHODS.has(name)) {
|
|
1696
|
+
current = call;
|
|
1697
|
+
continue;
|
|
1698
|
+
}
|
|
1699
|
+
return HANDLED;
|
|
1700
|
+
}
|
|
1701
|
+
case "CallExpression": {
|
|
1702
|
+
if (get(parent, "callee") === current) return HANDLED;
|
|
1703
|
+
const callee = unwrap(asNode(get(parent, "callee")) ?? parent);
|
|
1704
|
+
const name = memberName(callee);
|
|
1705
|
+
if (name === "waitUntil") return HANDLED;
|
|
1706
|
+
const object = callee.type === "MemberExpression" ? unwrap(asNode(get(callee, "object")) ?? callee) : null;
|
|
1707
|
+
if (object?.type === "Identifier" && object.name === "Promise" && name != null && PROMISE_COMBINATORS.has(name)) {
|
|
1708
|
+
current = parent;
|
|
1709
|
+
currentShape = "promise";
|
|
1710
|
+
continue;
|
|
1711
|
+
}
|
|
1712
|
+
return HANDLED;
|
|
1713
|
+
}
|
|
1714
|
+
case "VariableDeclarator": {
|
|
1715
|
+
const id = asNode(get(parent, "id"));
|
|
1716
|
+
if (get(parent, "init") !== current || id?.type !== "Identifier") return HANDLED;
|
|
1717
|
+
return analysis.mentioned.has(id.name) ? HANDLED : DROPPED;
|
|
1718
|
+
}
|
|
1719
|
+
case "AssignmentExpression": {
|
|
1720
|
+
if (get(parent, "right") !== current) return HANDLED;
|
|
1721
|
+
const name = getAssignmentTargetName(asNode(get(parent, "left")) ?? parent);
|
|
1722
|
+
if (name == null) return HANDLED;
|
|
1723
|
+
return analysis.mentioned.has(name) ? HANDLED : DROPPED;
|
|
1724
|
+
}
|
|
1725
|
+
default: return HANDLED;
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
/** Reports every place in `listener` where a delivery promise is dropped. */
|
|
1730
|
+
function checkListener(listener, report) {
|
|
1731
|
+
const scopes = [];
|
|
1732
|
+
walkUsedScopes(listener.body, (scope) => scopes.push(scope));
|
|
1733
|
+
const targets = findDeliveryTargets(listener, scopes);
|
|
1734
|
+
if (targets.contextName == null && targets.aliases.size === 0) return;
|
|
1735
|
+
const isDeliveryCall = (call) => {
|
|
1736
|
+
const callee = unwrap(call.callee);
|
|
1737
|
+
if (callee.type === "MemberExpression") {
|
|
1738
|
+
const name = memberName(callee);
|
|
1739
|
+
if (name == null || !DELIVERY_METHOD_NAMES.has(name)) return false;
|
|
1740
|
+
const object = unwrap(asNode(get(callee, "object")) ?? callee);
|
|
1741
|
+
return object.type === "Identifier" && object.name === targets.contextName;
|
|
1742
|
+
}
|
|
1743
|
+
return callee.type === "Identifier" && targets.aliases.has(callee.name);
|
|
1744
|
+
};
|
|
1745
|
+
const mentioned = /* @__PURE__ */ new Set();
|
|
1746
|
+
collectReferencedNames(listener.body, mentioned, true);
|
|
1747
|
+
const analysis = {
|
|
1748
|
+
listener,
|
|
1749
|
+
mentioned
|
|
1750
|
+
};
|
|
1751
|
+
const callsByScope = scopes.map((scope) => {
|
|
1752
|
+
const calls = [];
|
|
1753
|
+
for (const statement of scope.statements) collectCalls(statement, calls);
|
|
1754
|
+
return calls;
|
|
1755
|
+
});
|
|
1756
|
+
const carrying = /* @__PURE__ */ new Map();
|
|
1757
|
+
const carriesDelivery = (call, scope) => {
|
|
1758
|
+
if (isDeliveryCall(call)) return true;
|
|
1759
|
+
const callee = unwrap(call.callee);
|
|
1760
|
+
if (callee.type !== "Identifier") return false;
|
|
1761
|
+
return scope.functionsByName.get(callee.name)?.some((helper) => carrying.has(helper)) ?? false;
|
|
1762
|
+
};
|
|
1763
|
+
const noteCarrying = ({ fate, owner }) => {
|
|
1764
|
+
if (owner == null || owner === listener) return false;
|
|
1765
|
+
if (fate !== "awaited" && fate !== "returned") return false;
|
|
1766
|
+
const known = carrying.get(owner);
|
|
1767
|
+
if (known === "awaited" || known === fate) return false;
|
|
1768
|
+
carrying.set(owner, fate);
|
|
1769
|
+
return true;
|
|
1770
|
+
};
|
|
1771
|
+
for (let changed = true; changed;) {
|
|
1772
|
+
changed = false;
|
|
1773
|
+
scopes.forEach((scope, index) => {
|
|
1774
|
+
for (const call of callsByScope[index]) if (carriesDelivery(call, scope) && noteCarrying(fateOf(call, analysis))) changed = true;
|
|
1775
|
+
});
|
|
1776
|
+
}
|
|
1777
|
+
const reported = /* @__PURE__ */ new Set();
|
|
1778
|
+
const flag = (node) => {
|
|
1779
|
+
if (reported.has(node)) return;
|
|
1780
|
+
reported.add(node);
|
|
1781
|
+
report(node);
|
|
1782
|
+
};
|
|
1783
|
+
scopes.forEach((scope, index) => {
|
|
1784
|
+
for (const call of callsByScope[index]) {
|
|
1785
|
+
if (!carriesDelivery(call, scope)) continue;
|
|
1786
|
+
if (fateOf(call, analysis).fate === "dropped") flag(call);
|
|
1787
|
+
}
|
|
1788
|
+
});
|
|
1789
|
+
for (const [fn, kind] of carrying) if (kind === "awaited" && returnedOutcome(fn, analysis).fate === "dropped") flag(fn);
|
|
1790
|
+
scopes.forEach((scope, index) => {
|
|
1791
|
+
for (const call of callsByScope[index]) for (const arg of call.arguments) {
|
|
1792
|
+
if (arg.type !== "Identifier") continue;
|
|
1793
|
+
const held = scope.functionsByName.get(arg.name);
|
|
1794
|
+
if (held == null || !held.some((fn) => carrying.has(fn))) continue;
|
|
1795
|
+
if (callbackResultOutcome(call, analysis).fate === "dropped") flag(arg);
|
|
1796
|
+
}
|
|
1797
|
+
});
|
|
1798
|
+
}
|
|
1799
|
+
function createRule$1(buildReport) {
|
|
1800
|
+
return (context) => createOutboxListenerVisitor((listener) => checkListener(listener, (node) => {
|
|
1801
|
+
context.report({
|
|
1802
|
+
node,
|
|
1803
|
+
...buildReport
|
|
1804
|
+
});
|
|
1805
|
+
}));
|
|
1806
|
+
}
|
|
1807
|
+
const eslint$1 = {
|
|
1808
|
+
meta: {
|
|
1809
|
+
type: "suggestion",
|
|
1810
|
+
docs: { description: "Warn when an outbox listener delivers an activity without awaiting it" },
|
|
1811
|
+
schema: [],
|
|
1812
|
+
messages: { notAwaited: "{{ message }}" }
|
|
1813
|
+
},
|
|
1814
|
+
create: createRule$1({
|
|
1815
|
+
messageId: "notAwaited",
|
|
1816
|
+
data: { message: MESSAGE$1 }
|
|
1817
|
+
})
|
|
1818
|
+
};
|
|
1819
|
+
//#endregion
|
|
1820
|
+
//#region src/rules/outbox-listener-delivery-required.ts
|
|
1821
|
+
const MESSAGE = "Outbox listeners should deliver posted activities explicitly with ctx.sendActivity() or ctx.forwardActivity().";
|
|
1822
|
+
function escapeRegExp(value) {
|
|
1823
|
+
return value.replaceAll(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1824
|
+
}
|
|
1825
|
+
function stripCommentsAndStrings(code) {
|
|
1826
|
+
let result = "";
|
|
1827
|
+
let index = 0;
|
|
1828
|
+
const skipQuotedString = (quote) => {
|
|
1829
|
+
const start = index;
|
|
1830
|
+
index += 1;
|
|
1831
|
+
while (index < code.length) {
|
|
1832
|
+
const char = code[index];
|
|
1833
|
+
if (char === "\\") {
|
|
1834
|
+
index += 2;
|
|
1835
|
+
continue;
|
|
1836
|
+
}
|
|
1837
|
+
index += 1;
|
|
1838
|
+
if (char === quote) break;
|
|
1839
|
+
}
|
|
1840
|
+
const literal = code.slice(start, index);
|
|
1841
|
+
const value = literal.slice(1, -1);
|
|
1842
|
+
result += DELIVERY_METHOD_NAMES.has(value) ? literal : `${quote}${quote}`;
|
|
1843
|
+
};
|
|
1844
|
+
const stripTemplateLiteral = () => {
|
|
1845
|
+
const start = index;
|
|
1846
|
+
index += 1;
|
|
1847
|
+
let raw = "";
|
|
1848
|
+
let hasExpression = false;
|
|
1849
|
+
while (index < code.length) {
|
|
1850
|
+
const char = code[index];
|
|
1851
|
+
if (char === "\\") {
|
|
1852
|
+
raw += char;
|
|
1853
|
+
raw += code[index + 1] ?? "";
|
|
1854
|
+
index += 2;
|
|
1855
|
+
continue;
|
|
1856
|
+
}
|
|
1857
|
+
if (char === "`") {
|
|
1858
|
+
index += 1;
|
|
1859
|
+
if (!hasExpression && DELIVERY_METHOD_NAMES.has(raw)) result += code.slice(start, index);
|
|
1860
|
+
else result += "``";
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1863
|
+
if (char === "$" && code[index + 1] === "{") {
|
|
1864
|
+
hasExpression = true;
|
|
1865
|
+
result += "`${";
|
|
1866
|
+
index += 2;
|
|
1867
|
+
let depth = 1;
|
|
1868
|
+
while (index < code.length && depth > 0) {
|
|
1869
|
+
const exprChar = code[index];
|
|
1870
|
+
const next = code[index + 1];
|
|
1871
|
+
if (exprChar === "'" || exprChar === "\"") {
|
|
1872
|
+
skipQuotedString(exprChar);
|
|
1873
|
+
continue;
|
|
1874
|
+
}
|
|
1875
|
+
if (exprChar === "`") {
|
|
1876
|
+
stripTemplateLiteral();
|
|
1877
|
+
continue;
|
|
1878
|
+
}
|
|
1879
|
+
if (exprChar === "/" && next === "*") {
|
|
1880
|
+
index += 2;
|
|
1881
|
+
while (index < code.length) {
|
|
1882
|
+
if (code[index] === "*" && code[index + 1] === "/") {
|
|
1883
|
+
index += 2;
|
|
1884
|
+
break;
|
|
1885
|
+
}
|
|
1886
|
+
index += 1;
|
|
1887
|
+
}
|
|
1888
|
+
continue;
|
|
1889
|
+
}
|
|
1890
|
+
if (exprChar === "/" && next === "/") {
|
|
1891
|
+
index += 2;
|
|
1892
|
+
while (index < code.length && code[index] !== "\n") index += 1;
|
|
1893
|
+
continue;
|
|
1894
|
+
}
|
|
1895
|
+
result += exprChar;
|
|
1896
|
+
index += 1;
|
|
1897
|
+
if (exprChar === "{") depth += 1;
|
|
1898
|
+
else if (exprChar === "}") depth -= 1;
|
|
1899
|
+
}
|
|
1900
|
+
continue;
|
|
1901
|
+
}
|
|
1902
|
+
raw += char;
|
|
1903
|
+
index += 1;
|
|
1904
|
+
}
|
|
1905
|
+
result += "``";
|
|
1906
|
+
};
|
|
1907
|
+
while (index < code.length) {
|
|
1908
|
+
const char = code[index];
|
|
1909
|
+
const next = code[index + 1];
|
|
1910
|
+
if (char === "/" && next === "*") {
|
|
1911
|
+
index += 2;
|
|
1912
|
+
while (index < code.length) {
|
|
1913
|
+
if (code[index] === "*" && code[index + 1] === "/") {
|
|
1914
|
+
index += 2;
|
|
1915
|
+
break;
|
|
1916
|
+
}
|
|
1917
|
+
index += 1;
|
|
1918
|
+
}
|
|
1919
|
+
continue;
|
|
1920
|
+
}
|
|
1921
|
+
if (char === "/" && next === "/") {
|
|
1922
|
+
index += 2;
|
|
1923
|
+
while (index < code.length && code[index] !== "\n") index += 1;
|
|
1924
|
+
continue;
|
|
1925
|
+
}
|
|
1926
|
+
if (char === "'" || char === "\"") {
|
|
1927
|
+
skipQuotedString(char);
|
|
1928
|
+
continue;
|
|
1929
|
+
}
|
|
1930
|
+
if (char === "`") {
|
|
1931
|
+
stripTemplateLiteral();
|
|
1932
|
+
continue;
|
|
1933
|
+
}
|
|
1934
|
+
result += char;
|
|
1935
|
+
index += 1;
|
|
1936
|
+
}
|
|
1937
|
+
return result;
|
|
1938
|
+
}
|
|
1939
|
+
function getDeliveryAliasName(node) {
|
|
1940
|
+
if (node.type === "Identifier") return node.name;
|
|
1941
|
+
if (node.type === "AssignmentPattern" && node.left.type === "Identifier") return node.left.name;
|
|
1942
|
+
return null;
|
|
1943
|
+
}
|
|
1944
|
+
function buildContextExpressionPattern(contextName) {
|
|
1945
|
+
const name = escapeRegExp(contextName);
|
|
1946
|
+
const boundedName = String.raw`(?<![\w$])${name}(?![\w$])`;
|
|
1947
|
+
return String.raw`(?:${boundedName}|\(\s*${boundedName}(?:\s+as\s+[^)]+)?\s*\))`;
|
|
1948
|
+
}
|
|
1536
1949
|
/**
|
|
1537
1950
|
* Builds the source text to scan for a delivery call: the reachable
|
|
1538
1951
|
* statements of `root`, with every nested function literal either folded
|
|
@@ -1566,7 +1979,7 @@ function collectDeliveryScanCode(sourceCode, root, used, visited) {
|
|
|
1566
1979
|
for (const fn of byDescendingStart) {
|
|
1567
1980
|
const [fnStart, fnEnd] = getRange(fn);
|
|
1568
1981
|
const replacement = used.has(fn) ? collectDeliveryScanCode(sourceCode, fn.body, used, visited) : "";
|
|
1569
|
-
result = result.slice(0, fnStart - statementStart) +
|
|
1982
|
+
result = result.slice(0, fnStart - statementStart) + `\n${replacement.length > 0 ? replacement : "()=>{}"}\n` + result.slice(fnEnd - statementStart);
|
|
1570
1983
|
}
|
|
1571
1984
|
return result;
|
|
1572
1985
|
}).join("\n");
|
|
@@ -1603,37 +2016,14 @@ const listenerCallsDeliveryMethod = (sourceCode, listener) => {
|
|
|
1603
2016
|
};
|
|
1604
2017
|
function createRule(buildReport) {
|
|
1605
2018
|
return (context) => {
|
|
1606
|
-
const federationTracker = trackFederationVariables();
|
|
1607
|
-
const bindings = /* @__PURE__ */ new Map();
|
|
1608
|
-
const pendingCalls = [];
|
|
1609
2019
|
const sourceCode = context.sourceCode;
|
|
1610
|
-
|
|
1611
|
-
if (
|
|
1612
|
-
if (!isChainedFromOutboxListeners(node.callee.object, federationTracker)) return;
|
|
1613
|
-
const listener = node.arguments[1];
|
|
1614
|
-
const resolvedListener = isNode(listener) && isFunction(listener) ? listener : isNode(listener) ? resolveFunctionBinding(listener, bindings) : null;
|
|
1615
|
-
if (resolvedListener == null) return;
|
|
1616
|
-
if (listenerCallsDeliveryMethod(sourceCode, resolvedListener)) return;
|
|
2020
|
+
return createOutboxListenerVisitor((listener) => {
|
|
2021
|
+
if (listenerCallsDeliveryMethod(sourceCode, listener)) return;
|
|
1617
2022
|
context.report({
|
|
1618
|
-
node:
|
|
2023
|
+
node: listener,
|
|
1619
2024
|
...buildReport
|
|
1620
2025
|
});
|
|
1621
|
-
};
|
|
1622
|
-
return {
|
|
1623
|
-
VariableDeclarator(node) {
|
|
1624
|
-
federationTracker.VariableDeclarator(node);
|
|
1625
|
-
if (node.id.type === "Identifier" && node.init != null) bindings.set(node.id.name, node.init);
|
|
1626
|
-
},
|
|
1627
|
-
FunctionDeclaration(node) {
|
|
1628
|
-
if (node.id != null) bindings.set(node.id.name, node);
|
|
1629
|
-
},
|
|
1630
|
-
CallExpression(node) {
|
|
1631
|
-
pendingCalls.push(node);
|
|
1632
|
-
},
|
|
1633
|
-
"Program:exit"() {
|
|
1634
|
-
for (const node of pendingCalls) inspectCall(node);
|
|
1635
|
-
}
|
|
1636
|
-
};
|
|
2026
|
+
});
|
|
1637
2027
|
};
|
|
1638
2028
|
}
|
|
1639
2029
|
const eslint = {
|
|
@@ -1775,6 +2165,12 @@ Object.defineProperty(exports, "eslint$26", {
|
|
|
1775
2165
|
return eslint$26;
|
|
1776
2166
|
}
|
|
1777
2167
|
});
|
|
2168
|
+
Object.defineProperty(exports, "eslint$27", {
|
|
2169
|
+
enumerable: true,
|
|
2170
|
+
get: function() {
|
|
2171
|
+
return eslint$27;
|
|
2172
|
+
}
|
|
2173
|
+
});
|
|
1778
2174
|
Object.defineProperty(exports, "eslint$3", {
|
|
1779
2175
|
enumerable: true,
|
|
1780
2176
|
get: function() {
|