@fjall/eslint-plugin 13.1.0 → 14.1.0
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/no-optional-warning-emission.js +406 -342
- package/package.json +1 -1
|
@@ -8,32 +8,42 @@
|
|
|
8
8
|
* a bound sink even when no surface registered a callback: absence of a
|
|
9
9
|
* consumer means the spine's default sink, never silence.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
11
|
+
* Exactly two warning conventions exist (diagnostics-spine design, routing
|
|
12
|
+
* rule), and both satisfy this rule structurally — no allowlist remains:
|
|
13
|
+
* - Always-spine: `emitWarning(message, { scope })` (@fjall/util/
|
|
14
|
+
* diagnostics) wherever the CLI owns the terminal end of the lane.
|
|
15
|
+
* - External-boundary fallback: `emitWarningEvent`/`emitStepWarnings`
|
|
16
|
+
* (deploy-core orchestration/contextHelpers), serving the out-of-repo
|
|
17
|
+
* consumer of @fjall/deploy-core — present→callback, absent→spine. The
|
|
18
|
+
* helper's shape (guard the callback, call it, fall through to
|
|
19
|
+
* emitWarning) passes below precisely because its absent arm reaches
|
|
20
|
+
* the spine; the boundary, correctly implemented, is not an instance
|
|
21
|
+
* of the defect.
|
|
17
22
|
*
|
|
18
|
-
* Flagged
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
+
* Flagged shapes:
|
|
24
|
+
* 1. Optional-link emission: a callback invocation that can silently
|
|
25
|
+
* short-circuit (the call itself is `?.()`, or any member link in its
|
|
26
|
+
* callee chain is optional) whose callee names an `on[A-Z]`-shaped
|
|
27
|
+
* callback and whose arguments carry an object literal with
|
|
28
|
+
* `type: "warning"` (TS `as`/`satisfies` wrappers unwrapped,
|
|
29
|
+
* spread-extended literals included). A bare-identifier callee also
|
|
30
|
+
* counts as callback-named when its declaration initialiser reads an
|
|
31
|
+
* `on[A-Z]` callback (`const cb = options.onProgress`) — the alias
|
|
32
|
+
* does not launder the name.
|
|
33
|
+
* 2. Presence-guarded emission with no spine fallback: a PLAIN call of
|
|
34
|
+
* the same shape whose availability is guarded by the surrounding code
|
|
35
|
+
* (`if (cb) cb(…)`, `cb && cb(…)`, `cb ? cb(…) : …`, `typeof cb ===
|
|
36
|
+
* "function"`, `!!cb`, `Boolean(cb)`, including a guard on the callee
|
|
37
|
+
* chain's object) where the absent arm never reaches emitWarning/
|
|
38
|
+
* emitWarningEvent/emitStepWarnings — no else/alternate spine call,
|
|
39
|
+
* no fall-through spine call after the guard, no `||` spine
|
|
40
|
+
* continuation. The same silent drop as shape 1, laundered through a
|
|
41
|
+
* guard instead of `?.`.
|
|
23
42
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* optionally narrows by:
|
|
29
|
-
* - `enclosingFunction` — the nearest NAMED enclosing function; property-
|
|
30
|
-
* assigned and variable-assigned arrows count as named, anonymous
|
|
31
|
-
* wrappers (returned arrows, IIFEs) are skipped upward;
|
|
32
|
-
* - `withinCallee` — the site sits inside an ARGUMENT of a call or `new`
|
|
33
|
-
* of this name (callee identifier, or member property for method calls);
|
|
34
|
-
* - `messageProperty` — the event literal's `message` value is a read of
|
|
35
|
-
* this property (or a bare identifier of this name).
|
|
36
|
-
* All fields given on an entry must match for the entry to sanction a site.
|
|
43
|
+
* Both shapes see through the `.call`/`.apply` spellings: the invoked
|
|
44
|
+
* function is the member one link up (`options.onProgress.call(…)` reads
|
|
45
|
+
* as onProgress; `emitWarning.call(…)` still counts as the spine), and an
|
|
46
|
+
* `.apply` argument array is scanned for the warning literal.
|
|
37
47
|
*
|
|
38
48
|
* Known limitations (deliberate, matching the plugin's untyped design):
|
|
39
49
|
* - Literal-keyed only: an event object built in a binding and passed
|
|
@@ -44,197 +54,42 @@
|
|
|
44
54
|
* the rule cannot tell the two onWarning arities apart.
|
|
45
55
|
* - Non-callback-named emitters (`emit?.({ type: "warning" })`) pass — the
|
|
46
56
|
* spine's own sink plumbing legitimately holds that shape.
|
|
57
|
+
* - Guard analysis is intra-function: a guarded emission deferred into a
|
|
58
|
+
* nested closure, and the inverted early-exit guard (`if (!cb) return;
|
|
59
|
+
* cb(…)`), pass unexamined.
|
|
60
|
+
* - Alias resolution reads declaration initialisers only: a callback
|
|
61
|
+
* assigned to a pre-declared binding after the fact (`let cb; cb =
|
|
62
|
+
* options.onProgress;`) passes unexamined. `.bind` aliases (`const cb =
|
|
63
|
+
* options.onProgress?.bind(x)`) pass too — the initialiser is a call
|
|
64
|
+
* result, which alias resolution does not follow.
|
|
65
|
+
* - A guard hoisted into a boolean binding (`const hasCb =
|
|
66
|
+
* !!options.onProgress; if (hasCb) options.onProgress(…)`) passes
|
|
67
|
+
* unexamined — the guard's key no longer names the callee chain.
|
|
68
|
+
* - Absent-path reachability treats a nested function as reachable only
|
|
69
|
+
* from an invoked position (IIFE callee or call argument): a spine call
|
|
70
|
+
* inside a merely-declared closure does not satisfy the fallback, and
|
|
71
|
+
* conversely a fallback wrapped deeper (e.g. a closure stored in an
|
|
72
|
+
* object property handed to a runner) is not credited — that shape flags
|
|
73
|
+
* and needs the direct spelling or a disable comment.
|
|
47
74
|
*
|
|
48
|
-
* Escape hatch:
|
|
49
|
-
*
|
|
50
|
-
*
|
|
75
|
+
* Escape hatch: `// eslint-disable-next-line
|
|
76
|
+
* fjall/no-optional-warning-emission -- <why>` for genuinely local
|
|
77
|
+
* one-offs; there is no allow option.
|
|
51
78
|
*/
|
|
52
79
|
|
|
53
80
|
const CALLBACK_RE = /^on[A-Z]/;
|
|
54
81
|
|
|
55
82
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
83
|
+
* Callees whose invocation counts as reaching the diagnostics spine on the
|
|
84
|
+
* absent-callback path: the always-spine emitter and the two
|
|
85
|
+
* external-boundary fallback helpers (which themselves fall back to
|
|
86
|
+
* emitWarning when no callback is wired).
|
|
60
87
|
*/
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
enclosingFunction: "warnUnattributedOperation"
|
|
67
|
-
},
|
|
68
|
-
// §5.5 gateUntrackedDeploy onWarning wiring — feeds the tracked-deploy
|
|
69
|
-
// policy gate, whose warn-and-proceed arm renders on the caller's surface.
|
|
70
|
-
{
|
|
71
|
-
file: "cli/src/services/deployment/applicationDeployment.ts",
|
|
72
|
-
withinCallee: "gateUntrackedDeploy"
|
|
73
|
-
},
|
|
74
|
-
// §5.6 accept-and-mark upgrade notice off the 201 — a single non-blocking
|
|
75
|
-
// per-deploy notice on the deploy surface's own warning arm.
|
|
76
|
-
{
|
|
77
|
-
file: "cli/src/services/deployment/applicationDeployment.ts",
|
|
78
|
-
enclosingFunction: "deployApplication",
|
|
79
|
-
messageProperty: "upgradeNotice"
|
|
80
|
-
},
|
|
81
|
-
// The spine helpers' own internals: their present→callback arm IS the
|
|
82
|
-
// external-boundary fallback contract the rule points everyone else at.
|
|
83
|
-
{
|
|
84
|
-
file: "deploy-core/src/orchestration/contextHelpers.ts",
|
|
85
|
-
enclosingFunction: "emitWarningEvent"
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
file: "deploy-core/src/orchestration/contextHelpers.ts",
|
|
89
|
-
enclosingFunction: "emitStepWarnings"
|
|
90
|
-
},
|
|
91
|
-
{
|
|
92
|
-
file: "cli/src/services/deployment/callbackShared.ts",
|
|
93
|
-
enclosingFunction: "createOnLogHandler"
|
|
94
|
-
},
|
|
95
|
-
|
|
96
|
-
// Direct-producer lane — live-rendered via the surface builders' wired
|
|
97
|
-
// onProgress; migrates in the direct-producer-migration follow-on.
|
|
98
|
-
{
|
|
99
|
-
file: "cli/src/services/deployment/applicationDeployment.ts",
|
|
100
|
-
enclosingFunction: "warnBestEffort"
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
file: "cli/src/services/deployment/applicationDeployment.ts",
|
|
104
|
-
enclosingFunction: "deployApplication"
|
|
105
|
-
},
|
|
106
|
-
// DeploymentTracker onWarning wiring arrow.
|
|
107
|
-
{
|
|
108
|
-
file: "cli/src/services/deployment/applicationDeployment.ts",
|
|
109
|
-
enclosingFunction: "onWarning"
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
file: "cli/src/services/deployment/applicationDestruction.ts",
|
|
113
|
-
enclosingFunction: "destroyApplication"
|
|
114
|
-
},
|
|
115
|
-
{
|
|
116
|
-
file: "cli/src/services/deployment/applicationDestruction.ts",
|
|
117
|
-
enclosingFunction: "onWarning"
|
|
118
|
-
},
|
|
119
|
-
{
|
|
120
|
-
file: "cli/src/services/deployment/applicationDestruction.ts",
|
|
121
|
-
enclosingFunction: "trackAndReturnFailure"
|
|
122
|
-
},
|
|
123
|
-
{
|
|
124
|
-
file: "cli/src/services/deployment/applicationRestart.ts",
|
|
125
|
-
enclosingFunction: "warnBestEffort"
|
|
126
|
-
},
|
|
127
|
-
{
|
|
128
|
-
file: "cli/src/services/deployment/applicationRestart.ts",
|
|
129
|
-
enclosingFunction: "onWarning"
|
|
130
|
-
},
|
|
131
|
-
// Quarantine/retained-bucket cleanup signals on both adapter copies.
|
|
132
|
-
{
|
|
133
|
-
file: "cli/src/services/deployment/baseCallbackBuilder.ts",
|
|
134
|
-
enclosingFunction: "onStackCleanupProgress"
|
|
135
|
-
},
|
|
136
|
-
{
|
|
137
|
-
file: "cli/src/services/deployment/InteractiveCallbackAdapter.ts",
|
|
138
|
-
enclosingFunction: "onStackCleanupProgress"
|
|
139
|
-
},
|
|
140
|
-
{
|
|
141
|
-
file: "cli/src/services/deployment/orgCallbackMapper.ts",
|
|
142
|
-
enclosingFunction: "onCascadeLedger"
|
|
143
|
-
},
|
|
144
|
-
|
|
145
|
-
// Import lane — importOperation wires onProgress on every production
|
|
146
|
-
// path; deliberately callback-based, revisited in the
|
|
147
|
-
// direct-producer-migration follow-on.
|
|
148
|
-
{
|
|
149
|
-
file: "cli/src/services/import/ImportService.ts",
|
|
150
|
-
enclosingFunction: "importResource"
|
|
151
|
-
},
|
|
152
|
-
{
|
|
153
|
-
file: "cli/src/services/import/importAdoptionHelpers.ts",
|
|
154
|
-
enclosingFunction: "generateAndWriteCode"
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
file: "cli/src/services/monitoring/DiscoveryService.ts",
|
|
158
|
-
enclosingFunction: "readS3BucketDetail"
|
|
159
|
-
},
|
|
160
|
-
{
|
|
161
|
-
file: "cli/src/services/monitoring/DiscoveryService.ts",
|
|
162
|
-
enclosingFunction: "discoverVpcs"
|
|
163
|
-
},
|
|
164
|
-
|
|
165
|
-
// Pre-existing stock, wiring unverified — catalogued as fresh census
|
|
166
|
-
// input for the follow-on census (ADR); audited then migrated in the
|
|
167
|
-
// direct-producer-migration follow-on.
|
|
168
|
-
{
|
|
169
|
-
file: "cli/src/commands/provisioning/restore.ts",
|
|
170
|
-
enclosingFunction: "onWarning"
|
|
171
|
-
},
|
|
172
|
-
{
|
|
173
|
-
file: "cli/src/operations/resources/secretsOperations.ts",
|
|
174
|
-
enclosingFunction: "applyDeclaration"
|
|
175
|
-
},
|
|
176
|
-
{
|
|
177
|
-
file: "cli/src/services/application/ApplicationDetectionHelpers.ts",
|
|
178
|
-
enclosingFunction: "reportValidationNotRun"
|
|
179
|
-
},
|
|
180
|
-
{
|
|
181
|
-
file: "cli/src/services/application/ApplicationDetectionHelpers.ts",
|
|
182
|
-
enclosingFunction: "validateSecrets"
|
|
183
|
-
},
|
|
184
|
-
{
|
|
185
|
-
file: "cli/src/services/auth/OidcBootstrapService.ts",
|
|
186
|
-
enclosingFunction: "establishOidcTrust"
|
|
187
|
-
},
|
|
188
|
-
{
|
|
189
|
-
file: "cli/src/services/aws-account/AwsAccountService.ts",
|
|
190
|
-
enclosingFunction: "buildProviderAccountsFromConfig"
|
|
191
|
-
},
|
|
192
|
-
{
|
|
193
|
-
file: "cli/src/services/aws/awsExecution.ts",
|
|
194
|
-
enclosingFunction: "executeWithAwsCredentials"
|
|
195
|
-
},
|
|
196
|
-
{
|
|
197
|
-
file: "cli/src/services/domain-dns/CliDomainDeployProvider.ts",
|
|
198
|
-
enclosingFunction: "deployDomain"
|
|
199
|
-
},
|
|
200
|
-
{
|
|
201
|
-
file: "cli/src/services/domain-dns/delegationService.ts",
|
|
202
|
-
enclosingFunction: "deployPhase"
|
|
203
|
-
},
|
|
204
|
-
{
|
|
205
|
-
file: "cli/src/services/organisation/OrganisationDeployService.ts",
|
|
206
|
-
enclosingFunction: "onWarn"
|
|
207
|
-
},
|
|
208
|
-
{
|
|
209
|
-
file: "cli/src/services/organisation/OrganisationDestroyService.ts",
|
|
210
|
-
enclosingFunction: "onWarning"
|
|
211
|
-
},
|
|
212
|
-
{
|
|
213
|
-
file: "cli/src/services/organisation/OrganisationDestroyService.ts",
|
|
214
|
-
enclosingFunction: "destroy"
|
|
215
|
-
},
|
|
216
|
-
{
|
|
217
|
-
file: "cli/src/services/organisation/OrganisationSetupService.ts",
|
|
218
|
-
enclosingFunction: "onWarning"
|
|
219
|
-
},
|
|
220
|
-
{
|
|
221
|
-
file: "cli/src/services/organisation/OrganisationSetupService.ts",
|
|
222
|
-
enclosingFunction: "runOrganisationSetup"
|
|
223
|
-
},
|
|
224
|
-
{
|
|
225
|
-
file: "cli/src/services/organisation/OrganisationStandaloneHelpers.ts",
|
|
226
|
-
enclosingFunction: "registerStandaloneAccount"
|
|
227
|
-
},
|
|
228
|
-
|
|
229
|
-
// Dormant reporter-warning twin: ProgressReporter.warning has no live
|
|
230
|
-
// production instantiation post-tap; kept until the reporter-lane-reroute
|
|
231
|
-
// follow-on (a diagnostics import into the types module would be wrong
|
|
232
|
-
// layering).
|
|
233
|
-
{
|
|
234
|
-
file: "deploy-core/src/types/ProgressEvent.ts",
|
|
235
|
-
enclosingFunction: "warning"
|
|
236
|
-
}
|
|
237
|
-
];
|
|
88
|
+
const SPINE_CALLEES = new Set([
|
|
89
|
+
"emitWarning",
|
|
90
|
+
"emitWarningEvent",
|
|
91
|
+
"emitStepWarnings"
|
|
92
|
+
]);
|
|
238
93
|
|
|
239
94
|
/**
|
|
240
95
|
* Strip TS type-level wrappers so `"warning" as const` and
|
|
@@ -246,7 +101,8 @@ function unwrapTypeExpressions(node) {
|
|
|
246
101
|
current.type === "TSAsExpression" ||
|
|
247
102
|
current.type === "TSSatisfiesExpression" ||
|
|
248
103
|
current.type === "TSNonNullExpression" ||
|
|
249
|
-
current.type === "TSTypeAssertion"
|
|
104
|
+
current.type === "TSTypeAssertion" ||
|
|
105
|
+
current.type === "ChainExpression"
|
|
250
106
|
) {
|
|
251
107
|
current = current.expression;
|
|
252
108
|
}
|
|
@@ -269,7 +125,12 @@ function invocationCanSilentlySkip(node) {
|
|
|
269
125
|
return false;
|
|
270
126
|
}
|
|
271
127
|
|
|
272
|
-
/**
|
|
128
|
+
/**
|
|
129
|
+
* Terminal callee name: bare identifier, or member property. The
|
|
130
|
+
* `.call`/`.apply` spellings invoke their OBJECT, so they are named one
|
|
131
|
+
* link up when the object has a stable name (falling back to the literal
|
|
132
|
+
* property name when it does not).
|
|
133
|
+
*/
|
|
273
134
|
function terminalCalleeName(callee) {
|
|
274
135
|
let current = callee;
|
|
275
136
|
if (current.type === "ChainExpression") current = current.expression;
|
|
@@ -279,6 +140,10 @@ function terminalCalleeName(callee) {
|
|
|
279
140
|
!current.computed &&
|
|
280
141
|
current.property.type === "Identifier"
|
|
281
142
|
) {
|
|
143
|
+
if (current.property.name === "call" || current.property.name === "apply") {
|
|
144
|
+
const objectName = terminalCalleeName(current.object);
|
|
145
|
+
if (objectName !== null) return objectName;
|
|
146
|
+
}
|
|
282
147
|
return current.property.name;
|
|
283
148
|
}
|
|
284
149
|
return null;
|
|
@@ -292,18 +157,44 @@ function isPropertyNamed(property, name) {
|
|
|
292
157
|
return false;
|
|
293
158
|
}
|
|
294
159
|
|
|
160
|
+
/** Object literal carrying `type: "warning"`? */
|
|
161
|
+
function isWarningEventLiteral(argument) {
|
|
162
|
+
if (argument.type !== "ObjectExpression") return false;
|
|
163
|
+
for (const property of argument.properties) {
|
|
164
|
+
if (!isPropertyNamed(property, "type")) continue;
|
|
165
|
+
const value = unwrapTypeExpressions(property.value);
|
|
166
|
+
if (value.type === "Literal" && value.value === "warning") {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** True when the call is spelled through `.apply` (array-of-args form). */
|
|
174
|
+
function isApplySpelling(callee) {
|
|
175
|
+
let current = callee;
|
|
176
|
+
if (current.type === "ChainExpression") current = current.expression;
|
|
177
|
+
return (
|
|
178
|
+
current.type === "MemberExpression" &&
|
|
179
|
+
!current.computed &&
|
|
180
|
+
current.property.type === "Identifier" &&
|
|
181
|
+
current.property.name === "apply"
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
|
|
295
185
|
/**
|
|
296
|
-
* First argument object literal carrying `type: "warning"`, else null.
|
|
186
|
+
* First argument object literal carrying `type: "warning"`, else null. For
|
|
187
|
+
* the `.apply` spelling the argument array's elements are scanned too.
|
|
297
188
|
*/
|
|
298
189
|
function findWarningEventLiteral(node) {
|
|
299
190
|
for (const rawArgument of node.arguments) {
|
|
300
191
|
const argument = unwrapTypeExpressions(rawArgument);
|
|
301
|
-
if (argument
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
return
|
|
192
|
+
if (isWarningEventLiteral(argument)) return argument;
|
|
193
|
+
if (argument.type === "ArrayExpression" && isApplySpelling(node.callee)) {
|
|
194
|
+
for (const rawElement of argument.elements) {
|
|
195
|
+
if (rawElement === null) continue;
|
|
196
|
+
const element = unwrapTypeExpressions(rawElement);
|
|
197
|
+
if (isWarningEventLiteral(element)) return element;
|
|
307
198
|
}
|
|
308
199
|
}
|
|
309
200
|
}
|
|
@@ -311,145 +202,321 @@ function findWarningEventLiteral(node) {
|
|
|
311
202
|
}
|
|
312
203
|
|
|
313
204
|
/**
|
|
314
|
-
*
|
|
315
|
-
*
|
|
205
|
+
* True when a bare-identifier callee aliases an `on[A-Z]`-named callback:
|
|
206
|
+
* its resolved declaration's initialiser is (a member read of) a
|
|
207
|
+
* callback-named expression — `const cb = options.onProgress` /
|
|
208
|
+
* `= callbacks?.onProgress` / `= onProgress`. Declaration initialisers
|
|
209
|
+
* only; later reassignment is a documented pass.
|
|
316
210
|
*/
|
|
317
|
-
function
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
) {
|
|
322
|
-
return fn.id.name;
|
|
211
|
+
function aliasesCallbackName(callee, sourceCode) {
|
|
212
|
+
let calleeNode = callee;
|
|
213
|
+
if (calleeNode.type === "ChainExpression") {
|
|
214
|
+
calleeNode = calleeNode.expression;
|
|
323
215
|
}
|
|
324
|
-
|
|
325
|
-
if (!parent) return null;
|
|
216
|
+
// `cb.call(…)`/`cb.apply(…)` invoke the alias one link up.
|
|
326
217
|
if (
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
218
|
+
calleeNode.type === "MemberExpression" &&
|
|
219
|
+
!calleeNode.computed &&
|
|
220
|
+
calleeNode.property.type === "Identifier" &&
|
|
221
|
+
(calleeNode.property.name === "call" ||
|
|
222
|
+
calleeNode.property.name === "apply")
|
|
330
223
|
) {
|
|
331
|
-
|
|
224
|
+
calleeNode = calleeNode.object;
|
|
332
225
|
}
|
|
333
|
-
if (
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
226
|
+
if (calleeNode.type !== "Identifier") return false;
|
|
227
|
+
|
|
228
|
+
let scope = sourceCode.getScope(calleeNode);
|
|
229
|
+
let variable = null;
|
|
230
|
+
while (scope !== null && variable === null) {
|
|
231
|
+
variable = scope.variables.find((v) => v.name === calleeNode.name) ?? null;
|
|
232
|
+
scope = scope.upper;
|
|
338
233
|
}
|
|
234
|
+
if (variable === null) return false;
|
|
235
|
+
|
|
236
|
+
for (const def of variable.defs) {
|
|
237
|
+
if (def.type !== "Variable" || def.node.init === null) continue;
|
|
238
|
+
const init = unwrapTypeExpressions(def.node.init);
|
|
239
|
+
const initName = terminalCalleeName(init);
|
|
240
|
+
if (initName !== null && CALLBACK_RE.test(initName)) return true;
|
|
241
|
+
}
|
|
242
|
+
return false;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** Function-node predicate shared by the guard and reachability walks. */
|
|
246
|
+
function isFunctionNode(node) {
|
|
247
|
+
return (
|
|
248
|
+
node.type === "FunctionDeclaration" ||
|
|
249
|
+
node.type === "FunctionExpression" ||
|
|
250
|
+
node.type === "ArrowFunctionExpression"
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Canonical spelling of a presence-testable expression: an identifier,
|
|
256
|
+
* `this`, or a non-computed member chain over those. Optional links and TS
|
|
257
|
+
* wrappers are normalised away, so `callbacks?.onProgress` and
|
|
258
|
+
* `callbacks.onProgress` share a key. Null when the expression has no
|
|
259
|
+
* stable spelling (computed member, call result, …).
|
|
260
|
+
*/
|
|
261
|
+
function expressionKey(node) {
|
|
262
|
+
const current = unwrapTypeExpressions(node);
|
|
263
|
+
if (current.type === "Identifier") return current.name;
|
|
264
|
+
if (current.type === "ThisExpression") return "this";
|
|
339
265
|
if (
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
parent.key.type === "Identifier"
|
|
266
|
+
current.type === "MemberExpression" &&
|
|
267
|
+
!current.computed &&
|
|
268
|
+
current.property.type === "Identifier"
|
|
344
269
|
) {
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
if (parent.type === "AssignmentExpression" && parent.right === fn) {
|
|
348
|
-
if (parent.left.type === "Identifier") return parent.left.name;
|
|
349
|
-
if (
|
|
350
|
-
parent.left.type === "MemberExpression" &&
|
|
351
|
-
!parent.left.computed &&
|
|
352
|
-
parent.left.property.type === "Identifier"
|
|
353
|
-
) {
|
|
354
|
-
return parent.left.property.name;
|
|
355
|
-
}
|
|
270
|
+
const objectKey = expressionKey(current.object);
|
|
271
|
+
return objectKey === null ? null : `${objectKey}.${current.property.name}`;
|
|
356
272
|
}
|
|
357
273
|
return null;
|
|
358
274
|
}
|
|
359
275
|
|
|
360
276
|
/**
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
277
|
+
* True when establishing `guardKey` present also establishes the call path
|
|
278
|
+
* to `calleeKey`: the same expression, or a prefix of its member chain
|
|
279
|
+
* (guarding `callbacks` guards `callbacks.onProgress`).
|
|
364
280
|
*/
|
|
365
|
-
function
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
281
|
+
function keyGuardsCallee(guardKey, calleeKey) {
|
|
282
|
+
if (guardKey === null) return false;
|
|
283
|
+
return calleeKey === guardKey || calleeKey.startsWith(`${guardKey}.`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/** `undefined`, `null`, or `void …` — the absence spellings in guards. */
|
|
287
|
+
function isUndefinedLike(node) {
|
|
288
|
+
const value = unwrapTypeExpressions(node);
|
|
289
|
+
if (value.type === "Identifier" && value.name === "undefined") return true;
|
|
290
|
+
if (value.type === "Literal" && value.value === null) return true;
|
|
291
|
+
if (value.type === "UnaryExpression" && value.operator === "void") {
|
|
292
|
+
return true;
|
|
377
293
|
}
|
|
378
|
-
return
|
|
294
|
+
return false;
|
|
379
295
|
}
|
|
380
296
|
|
|
381
297
|
/**
|
|
382
|
-
* True when
|
|
383
|
-
*
|
|
384
|
-
* emission and the sanctioning call is expected.
|
|
298
|
+
* True when one side of `binary` is `typeof <expr guarding calleeKey>` and
|
|
299
|
+
* the other is the string literal `literalValue`.
|
|
385
300
|
*/
|
|
386
|
-
function
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
301
|
+
function typeofComparedTo(binary, calleeKey, literalValue) {
|
|
302
|
+
const sides = [
|
|
303
|
+
[binary.left, binary.right],
|
|
304
|
+
[binary.right, binary.left]
|
|
305
|
+
];
|
|
306
|
+
for (const [candidate, literal] of sides) {
|
|
307
|
+
const typeofSide = unwrapTypeExpressions(candidate);
|
|
308
|
+
const literalSide = unwrapTypeExpressions(literal);
|
|
390
309
|
if (
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
310
|
+
typeofSide.type === "UnaryExpression" &&
|
|
311
|
+
typeofSide.operator === "typeof" &&
|
|
312
|
+
keyGuardsCallee(expressionKey(typeofSide.argument), calleeKey) &&
|
|
313
|
+
literalSide.type === "Literal" &&
|
|
314
|
+
literalSide.value === literalValue
|
|
394
315
|
) {
|
|
395
316
|
return true;
|
|
396
317
|
}
|
|
397
|
-
previous = current;
|
|
398
|
-
current = current.parent;
|
|
399
318
|
}
|
|
400
319
|
return false;
|
|
401
320
|
}
|
|
402
321
|
|
|
403
322
|
/**
|
|
404
|
-
* True when
|
|
405
|
-
*
|
|
406
|
-
*
|
|
323
|
+
* True when `test` (or an `&&`-conjunct of it) establishes that the callee
|
|
324
|
+
* spelled `calleeKey` is present/callable: bare truthiness, `!!` double
|
|
325
|
+
* negation, `Boolean(…)`, `!== undefined` / `!= null`, `typeof … !==
|
|
326
|
+
* "undefined"`, or `typeof … === "function"` — over the callee expression
|
|
327
|
+
* itself or a prefix of its member chain.
|
|
407
328
|
*/
|
|
408
|
-
function
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
)
|
|
418
|
-
|
|
329
|
+
function testGuardsPresence(test, calleeKey) {
|
|
330
|
+
const condition = unwrapTypeExpressions(test);
|
|
331
|
+
if (condition.type === "LogicalExpression" && condition.operator === "&&") {
|
|
332
|
+
return (
|
|
333
|
+
testGuardsPresence(condition.left, calleeKey) ||
|
|
334
|
+
testGuardsPresence(condition.right, calleeKey)
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
if (condition.type === "UnaryExpression" && condition.operator === "!") {
|
|
338
|
+
const inner = unwrapTypeExpressions(condition.argument);
|
|
339
|
+
return (
|
|
340
|
+
inner.type === "UnaryExpression" &&
|
|
341
|
+
inner.operator === "!" &&
|
|
342
|
+
testGuardsPresence(inner.argument, calleeKey)
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
if (
|
|
346
|
+
condition.type === "CallExpression" &&
|
|
347
|
+
condition.callee.type === "Identifier" &&
|
|
348
|
+
condition.callee.name === "Boolean" &&
|
|
349
|
+
condition.arguments.length === 1
|
|
350
|
+
) {
|
|
351
|
+
return testGuardsPresence(condition.arguments[0], calleeKey);
|
|
352
|
+
}
|
|
353
|
+
if (keyGuardsCallee(expressionKey(condition), calleeKey)) return true;
|
|
354
|
+
if (condition.type === "BinaryExpression") {
|
|
355
|
+
const { operator } = condition;
|
|
356
|
+
if (operator === "!==" || operator === "!=") {
|
|
357
|
+
if (
|
|
358
|
+
(isUndefinedLike(condition.right) &&
|
|
359
|
+
keyGuardsCallee(expressionKey(condition.left), calleeKey)) ||
|
|
360
|
+
(isUndefinedLike(condition.left) &&
|
|
361
|
+
keyGuardsCallee(expressionKey(condition.right), calleeKey))
|
|
362
|
+
) {
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
if (typeofComparedTo(condition, calleeKey, "undefined")) return true;
|
|
366
|
+
}
|
|
367
|
+
if (operator === "===" || operator === "==") {
|
|
368
|
+
if (typeofComparedTo(condition, calleeKey, "function")) return true;
|
|
419
369
|
}
|
|
420
370
|
}
|
|
421
371
|
return false;
|
|
422
372
|
}
|
|
423
373
|
|
|
424
|
-
/**
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
374
|
+
/**
|
|
375
|
+
* Innermost construct that presence-guards `calleeKey` around `node`: an
|
|
376
|
+
* IfStatement/ConditionalExpression whose test guards the callee with the
|
|
377
|
+
* node in its consequent, or a `&&` whose left guards it with the node on
|
|
378
|
+
* the right. Intra-function — the walk stops at the enclosing function
|
|
379
|
+
* boundary (a guarded emission deferred into a nested closure is a
|
|
380
|
+
* documented pass).
|
|
381
|
+
*/
|
|
382
|
+
function findPresenceGuard(node, calleeKey) {
|
|
383
|
+
let previous = node;
|
|
384
|
+
let current = node.parent;
|
|
385
|
+
while (current && !isFunctionNode(current)) {
|
|
433
386
|
if (
|
|
434
|
-
|
|
435
|
-
|
|
387
|
+
current.type === "IfStatement" &&
|
|
388
|
+
previous === current.consequent &&
|
|
389
|
+
testGuardsPresence(current.test, calleeKey)
|
|
436
390
|
) {
|
|
437
|
-
|
|
391
|
+
return current;
|
|
438
392
|
}
|
|
439
393
|
if (
|
|
440
|
-
|
|
441
|
-
|
|
394
|
+
current.type === "ConditionalExpression" &&
|
|
395
|
+
previous === current.consequent &&
|
|
396
|
+
testGuardsPresence(current.test, calleeKey)
|
|
442
397
|
) {
|
|
443
|
-
|
|
398
|
+
return current;
|
|
444
399
|
}
|
|
445
400
|
if (
|
|
446
|
-
|
|
447
|
-
|
|
401
|
+
current.type === "LogicalExpression" &&
|
|
402
|
+
current.operator === "&&" &&
|
|
403
|
+
previous === current.right &&
|
|
404
|
+
testGuardsPresence(current.left, calleeKey)
|
|
448
405
|
) {
|
|
449
|
-
|
|
406
|
+
return current;
|
|
450
407
|
}
|
|
408
|
+
previous = current;
|
|
409
|
+
current = current.parent;
|
|
410
|
+
}
|
|
411
|
+
return null;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* True when a function-valued child sits in a position that invokes it (or
|
|
416
|
+
* hands it to something that may): the callee of a call (IIFE) or a call
|
|
417
|
+
* argument (callback given to a runner). A function value anywhere else — a
|
|
418
|
+
* declaration, a variable initialiser, an else-arm closure — executes
|
|
419
|
+
* nothing where it stands, so a spine call inside it must not count as the
|
|
420
|
+
* absent path reaching the spine.
|
|
421
|
+
*/
|
|
422
|
+
function functionChildMayRun(parent, child) {
|
|
423
|
+
return (
|
|
424
|
+
parent.type === "CallExpression" &&
|
|
425
|
+
(parent.callee === child || parent.arguments.includes(child))
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* True when `node`'s subtree contains a REACHABLE call to a spine callee.
|
|
431
|
+
* Recursive descent over AST children; never follows `parent` links.
|
|
432
|
+
* Nested function bodies are descended only from an invoked position
|
|
433
|
+
* (see functionChildMayRun) — a merely-declared closure mentioning
|
|
434
|
+
* emitWarning reaches nothing at runtime.
|
|
435
|
+
*/
|
|
436
|
+
function containsSpineCall(node) {
|
|
437
|
+
if (
|
|
438
|
+
node.type === "CallExpression" &&
|
|
439
|
+
SPINE_CALLEES.has(terminalCalleeName(node.callee) ?? "")
|
|
440
|
+
) {
|
|
441
|
+
return true;
|
|
442
|
+
}
|
|
443
|
+
for (const key of Object.keys(node)) {
|
|
444
|
+
if (key === "parent") continue;
|
|
445
|
+
const value = node[key];
|
|
446
|
+
if (Array.isArray(value)) {
|
|
447
|
+
for (const child of value) {
|
|
448
|
+
if (
|
|
449
|
+
child !== null &&
|
|
450
|
+
typeof child === "object" &&
|
|
451
|
+
typeof child.type === "string" &&
|
|
452
|
+
(!isFunctionNode(child) || functionChildMayRun(node, child)) &&
|
|
453
|
+
containsSpineCall(child)
|
|
454
|
+
) {
|
|
455
|
+
return true;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
} else if (
|
|
459
|
+
value !== null &&
|
|
460
|
+
typeof value === "object" &&
|
|
461
|
+
typeof value.type === "string" &&
|
|
462
|
+
(!isFunctionNode(value) || functionChildMayRun(node, value)) &&
|
|
463
|
+
containsSpineCall(value)
|
|
464
|
+
) {
|
|
465
|
+
return true;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* True when the absent-callback path of `guard` can still reach the spine:
|
|
473
|
+
* the guard's own alternate branch carries a spine call, a statement after
|
|
474
|
+
* the guard (walking enclosing blocks up to the function boundary) does —
|
|
475
|
+
* the boundary-helper shape guards, calls, returns, and falls through to
|
|
476
|
+
* emitWarning — or a `||` continuation to the guard's right does.
|
|
477
|
+
* Errs towards passing: partial reachability on exotic control flow is
|
|
478
|
+
* accepted rather than over-flagged.
|
|
479
|
+
*/
|
|
480
|
+
function absentPathReachesSpine(guard) {
|
|
481
|
+
if (
|
|
482
|
+
guard.type === "IfStatement" &&
|
|
483
|
+
guard.alternate !== null &&
|
|
484
|
+
containsSpineCall(guard.alternate)
|
|
485
|
+
) {
|
|
486
|
+
return true;
|
|
487
|
+
}
|
|
488
|
+
if (
|
|
489
|
+
guard.type === "ConditionalExpression" &&
|
|
490
|
+
containsSpineCall(guard.alternate)
|
|
491
|
+
) {
|
|
451
492
|
return true;
|
|
452
493
|
}
|
|
494
|
+
|
|
495
|
+
let previous = guard;
|
|
496
|
+
let current = guard.parent;
|
|
497
|
+
while (current && !isFunctionNode(current)) {
|
|
498
|
+
const body =
|
|
499
|
+
current.type === "BlockStatement" || current.type === "Program"
|
|
500
|
+
? current.body
|
|
501
|
+
: current.type === "SwitchCase"
|
|
502
|
+
? current.consequent
|
|
503
|
+
: null;
|
|
504
|
+
if (body !== null) {
|
|
505
|
+
const index = body.indexOf(previous);
|
|
506
|
+
for (let i = index + 1; i < body.length; i++) {
|
|
507
|
+
if (containsSpineCall(body[i])) return true;
|
|
508
|
+
}
|
|
509
|
+
} else if (
|
|
510
|
+
current.type === "LogicalExpression" &&
|
|
511
|
+
current.operator === "||" &&
|
|
512
|
+
previous === current.left &&
|
|
513
|
+
containsSpineCall(current.right)
|
|
514
|
+
) {
|
|
515
|
+
return true;
|
|
516
|
+
}
|
|
517
|
+
previous = current;
|
|
518
|
+
current = current.parent;
|
|
519
|
+
}
|
|
453
520
|
return false;
|
|
454
521
|
}
|
|
455
522
|
|
|
@@ -459,58 +526,55 @@ export default {
|
|
|
459
526
|
type: "problem",
|
|
460
527
|
docs: {
|
|
461
528
|
description:
|
|
462
|
-
"Disallow warning-typed events on optional callback links — an absent link silently swallows the warning; route it via the diagnostics spine",
|
|
529
|
+
"Disallow warning-typed events on optional or presence-guarded callback links with no spine fallback — an absent link silently swallows the warning; route it via the diagnostics spine",
|
|
463
530
|
category: "Possible Errors",
|
|
464
531
|
recommended: true
|
|
465
532
|
},
|
|
466
533
|
messages: {
|
|
467
534
|
optionalWarningEmission:
|
|
468
|
-
"Warning-typed event rides an optional callback link (`{{callee}}`) — when the link is absent the warning vanishes silently. Emit via emitWarning(message, { scope }) (@fjall/util/diagnostics) where the CLI owns the lane, or emitStepWarnings/emitWarningEvent (deploy-core contextHelpers) at the external-consumer boundary.
|
|
535
|
+
"Warning-typed event rides an optional callback link (`{{callee}}`) — when the link is absent the warning vanishes silently. Emit via emitWarning(message, { scope }) (@fjall/util/diagnostics) where the CLI owns the lane, or emitStepWarnings/emitWarningEvent (deploy-core contextHelpers) at the external-consumer boundary.",
|
|
536
|
+
guardedWarningEmission:
|
|
537
|
+
"Warning-typed event rides a presence-guarded callback (`{{callee}}`) whose absent arm never reaches the diagnostics spine — when the callback is absent the warning vanishes silently. Emit via emitWarning(message, { scope }) (@fjall/util/diagnostics) where the CLI owns the lane, or give the absent arm the external-boundary fallback shape (present→callback, absent→emitWarning; see deploy-core contextHelpers emitWarningEvent)."
|
|
469
538
|
},
|
|
470
|
-
schema: [
|
|
471
|
-
{
|
|
472
|
-
type: "object",
|
|
473
|
-
properties: {
|
|
474
|
-
allow: {
|
|
475
|
-
type: "array",
|
|
476
|
-
items: {
|
|
477
|
-
type: "object",
|
|
478
|
-
properties: {
|
|
479
|
-
file: { type: "string" },
|
|
480
|
-
enclosingFunction: { type: "string" },
|
|
481
|
-
withinCallee: { type: "string" },
|
|
482
|
-
messageProperty: { type: "string" }
|
|
483
|
-
},
|
|
484
|
-
required: ["file"],
|
|
485
|
-
additionalProperties: false
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
},
|
|
489
|
-
additionalProperties: false
|
|
490
|
-
}
|
|
491
|
-
]
|
|
539
|
+
schema: []
|
|
492
540
|
},
|
|
493
541
|
|
|
494
542
|
create(context) {
|
|
495
|
-
const allow = context.options[0]?.allow ?? [];
|
|
496
|
-
|
|
497
543
|
return {
|
|
498
544
|
CallExpression(node) {
|
|
499
|
-
if (!invocationCanSilentlySkip(node)) return;
|
|
500
|
-
|
|
501
545
|
const calleeName = terminalCalleeName(node.callee);
|
|
502
|
-
if (calleeName === null
|
|
546
|
+
if (calleeName === null) return;
|
|
547
|
+
if (
|
|
548
|
+
!CALLBACK_RE.test(calleeName) &&
|
|
549
|
+
!aliasesCallbackName(node.callee, context.sourceCode)
|
|
550
|
+
) {
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
503
553
|
|
|
504
554
|
const eventLiteral = findWarningEventLiteral(node);
|
|
505
555
|
if (eventLiteral === null) return;
|
|
506
556
|
|
|
507
|
-
if (
|
|
557
|
+
if (invocationCanSilentlySkip(node)) {
|
|
558
|
+
context.report({
|
|
559
|
+
node,
|
|
560
|
+
messageId: "optionalWarningEmission",
|
|
561
|
+
data: { callee: calleeName }
|
|
562
|
+
});
|
|
508
563
|
return;
|
|
509
564
|
}
|
|
510
565
|
|
|
566
|
+
// Hardening: a plain call laundered through a presence guard drops
|
|
567
|
+
// the warning just as silently when the absent arm has no spine
|
|
568
|
+
// fallback.
|
|
569
|
+
const calleeKey = expressionKey(node.callee);
|
|
570
|
+
if (calleeKey === null) return;
|
|
571
|
+
const guard = findPresenceGuard(node, calleeKey);
|
|
572
|
+
if (guard === null) return;
|
|
573
|
+
if (absentPathReachesSpine(guard)) return;
|
|
574
|
+
|
|
511
575
|
context.report({
|
|
512
576
|
node,
|
|
513
|
-
messageId: "
|
|
577
|
+
messageId: "guardedWarningEmission",
|
|
514
578
|
data: { callee: calleeName }
|
|
515
579
|
});
|
|
516
580
|
}
|