@suss/resolution 0.22.4 → 0.24.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/README.md +8 -337
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# @suss/resolution
|
|
2
2
|
|
|
3
|
+
Part of [suss](https://github.com/nimbuscloud-ai/suss), which reads both sides of every call in a repository and says where the two disagree.
|
|
4
|
+
|
|
3
5
|
The rules for following a value to the function it ends up being.
|
|
4
6
|
|
|
5
7
|
## What this package is
|
|
@@ -16,343 +18,12 @@ A function that returns a function calling its parameter hands back the
|
|
|
16
18
|
argument it was given, which is a decorator in Python, a closure in Go,
|
|
17
19
|
and a handler factory in TypeScript.
|
|
18
20
|
|
|
19
|
-
##
|
|
20
|
-
|
|
21
|
-
```
|
|
22
|
-
func(f) f is a function
|
|
23
|
-
objectValue(o) o is an object written out literally
|
|
24
|
-
writtenValue(x) x is an expression written out in source
|
|
25
|
-
rather than a name for one
|
|
26
|
-
placeholderValue(x) x is a written value a later write is
|
|
27
|
-
expected to replace, such as None or nil
|
|
28
|
-
holdsProperty(o, n, x) object o holds x under the name n
|
|
29
|
-
readsProperty(x, o, n) x is the expression o.n
|
|
30
|
-
binds(x, y) the name x is declared as y
|
|
31
|
-
endsHolding(x, y) the name x is written more than once and
|
|
32
|
-
holds y once the writes have run
|
|
33
|
-
fallbackBranch(x, b) x is a fallback expression and b is one
|
|
34
|
-
of its branches
|
|
35
|
-
paramOf(f, k, p) p is f's parameter at position k
|
|
36
|
-
paramNamed(f, n, p) p is f's parameter called n
|
|
37
|
-
extends(c, b) class c is written as extending b
|
|
38
|
-
returnsValue(f, v) f returns v
|
|
39
|
-
bodyCalls(f, c) f's body calls c
|
|
40
|
-
containsFn(f, g) g is declared inside f
|
|
41
|
-
call(r, c) r is a call whose callee is c
|
|
42
|
-
callArg(r, k, a) r passes a at position k
|
|
43
|
-
imports(x, m, n) x is the name n imported from module m
|
|
44
|
-
exportsAs(m, n, v) module m exports v under the name n
|
|
45
|
-
reExports(m, n, m2, n2) m's n is m2's n2
|
|
46
|
-
reExportsAll(m, m2) m forwards everything m2 exports
|
|
47
|
-
```
|
|
48
|
-
|
|
49
|
-
Two more come from a pack rather than from source, for a wrapper whose
|
|
50
|
-
body nobody can read: `unwrapsByName(name, k)` and
|
|
51
|
-
`wrapperModule(name, module)`. Both are checked against `calleeName` and
|
|
52
|
-
`calleeOrigin`, so a local function that happens to be spelled the same
|
|
53
|
-
as the library's does not match.
|
|
54
|
-
|
|
55
|
-
`holdsProperty` is something an adapter states and the rules only read.
|
|
56
|
-
What a value contains, a base class included, comes out as `contains`,
|
|
57
|
-
so a method a base declares is found on a subclass that never overrode
|
|
58
|
-
it. Deriving that into `holdsProperty` instead turns it from a fact into
|
|
59
|
-
a derived relation, and the on-demand rewrite then gates it behind a
|
|
60
|
-
demand nothing generates.
|
|
61
|
-
|
|
62
|
-
Node identity is the adapter's business. The rules only join on it.
|
|
63
|
-
|
|
64
|
-
## One relation of steps
|
|
65
|
-
|
|
66
|
-
Every construct states its hops once, in one relation:
|
|
67
|
-
|
|
68
|
-
```
|
|
69
|
-
stepsTo(x, y, kind) following x leads to y in one hop
|
|
70
|
-
reaches(x, z, kind) the closure of those hops
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
A value step goes to the value x is written as: a name to its
|
|
74
|
-
declaration, an import to what the module exports, a parameter to what a
|
|
75
|
-
call passes it, a property read to what the object contains under that
|
|
76
|
-
name, a construction to the class, a transparent wrapper to the argument
|
|
77
|
-
it wraps. A result step goes the other way, from a call to what the
|
|
78
|
-
function it invokes returns. A walk counts as a result walk once it has
|
|
79
|
-
run a call anywhere along it, which is what keeps a factory call out of
|
|
80
|
-
the answers that stop at a value.
|
|
81
|
-
|
|
82
|
-
A language with a hop of its own states it as a step too. JavaScript's
|
|
83
|
-
`.bind` and Ruby's `Const.new` are each one rule, and every question
|
|
84
|
-
below picks them up without being told.
|
|
85
|
-
|
|
86
|
-
A fallback expression (`a || b`, `a ?? b`, Python's `a or b`) says its
|
|
87
|
-
value is one of its branches, so each branch is a value step. That is
|
|
88
|
-
the whole rule. When one branch is something no static reader can
|
|
89
|
-
settle, a global cache or a parameter, that branch derives nothing, and
|
|
90
|
-
the branch that does resolve is the only claim the source makes. The
|
|
91
|
-
idiomatic client singleton, `global.prisma || new PrismaClient()`, is
|
|
92
|
-
exactly this: the global read makes no claim, so the construction is
|
|
93
|
-
the answer. When both branches resolve to different things, both
|
|
94
|
-
derive, and the caller's single-answer policy refuses the pair. That is
|
|
95
|
-
the same refusal every other two-candidate chain gets, because a value
|
|
96
|
-
that is one of two different things is not one thing.
|
|
97
|
-
|
|
98
|
-
Each question is that one closure with its own stopping condition. So
|
|
99
|
-
adding a construct is one step and every question gets it, and adding a
|
|
100
|
-
question is a stopping condition and no steps at all.
|
|
101
|
-
|
|
102
|
-
The closure is written with the walk so far first and the next hop after
|
|
103
|
-
it: `reaches(x, z) <- reaches(x, y), stepsTo(y, z)`. The order matters
|
|
104
|
-
under demand. Every question here asks about one value, and with the
|
|
105
|
-
walk first, the demand stays on that value and each round extends the
|
|
106
|
-
walks it already has by one hop. Written hop first, `reaches(x, z) <-
|
|
107
|
-
stepsTo(x, y), reaches(y, z)`, a question about `x` becomes a question
|
|
108
|
-
about every `y` a hop leads to, so the closure is derived from every
|
|
109
|
-
value the walk passes through, and each new walk is joined back against
|
|
110
|
-
every hop that lands on its start. Extracting the TypeScript adapter's
|
|
111
|
-
own sources, the hop first order spent two seconds in the closure, and
|
|
112
|
-
the walk first order spends forty milliseconds.
|
|
113
|
-
|
|
114
|
-
## What comes out
|
|
115
|
-
|
|
116
|
-
```
|
|
117
|
-
comesTo(x, z) following x arrives at the value z
|
|
118
|
-
resolves(x, z) comesTo narrowed to functions
|
|
119
|
-
givesBack(x, z) following x arrives at a call that returns z
|
|
120
|
-
isWrittenAs(x, z) x is written as the expression z
|
|
121
|
-
comesFrom(x, m, n) following x arrives at m's export n
|
|
122
|
-
callsInto(f, m, n) calling f ends up calling m's n
|
|
123
|
-
paramAt(r, p, z) the call r puts z in the parameter p
|
|
124
|
-
```
|
|
125
|
-
|
|
126
|
-
`resolves` is the question most callers ask. `comesTo` is the one
|
|
127
|
-
underneath it, and it can come back with an object, because a chain has
|
|
128
|
-
to pass through objects for `routes.list` to reach whatever `list`
|
|
129
|
-
contains.
|
|
130
|
-
|
|
131
|
-
`givesBack` is the other direction of function application. `comesTo`
|
|
132
|
-
says what a value comes down to, and it stops at a call on purpose: a
|
|
133
|
-
factory call is usually the wrapper itself, so resolving it to the
|
|
134
|
-
function it returned would fight the unwrapping answer. That leaves
|
|
135
|
-
nobody able to ask what a call returned, which is the whole question
|
|
136
|
-
when a factory builds a dependency. `givesBack` asks it, over the same
|
|
137
|
-
steps and with the same stopping condition, of the walks that ran a
|
|
138
|
-
call.
|
|
139
|
-
|
|
140
|
-
Both directions run at once without interfering, because they answer
|
|
141
|
-
different questions about the same call. `const dao = makeDao()` comes
|
|
142
|
-
to nothing and gives back the class `makeDao` constructed, so
|
|
143
|
-
`dao.findByCustomer` finds the method that class declares while
|
|
144
|
-
`withAuth(handler)` still comes to `handler`.
|
|
145
|
-
|
|
146
|
-
`isWrittenAs` follows the same names to the expression a value is
|
|
147
|
-
written as, whatever kind of expression that turns out to be. A GraphQL
|
|
148
|
-
document is neither a function nor an object, so `comesTo` never
|
|
149
|
-
reaches one.
|
|
150
|
-
|
|
151
|
-
`comesFrom` covers the direction `comesTo` cannot. Every `comesTo` chain
|
|
152
|
-
ends at something written out in the source suss is reading, so a name
|
|
153
|
-
for a library's own function ends nowhere, because the library's body is
|
|
154
|
-
not here. `comesFrom` walks the same steps and stops at the import
|
|
155
|
-
instead, so it comes back with the module plus the name that module
|
|
156
|
-
exports.
|
|
157
|
-
|
|
158
|
-
`paramAt` is the one question that keeps the call it went through.
|
|
159
|
-
`comesTo` merges call sites, so a function called from two places leaves
|
|
160
|
-
its parameter with two values and a caller wanting one gets nothing.
|
|
161
|
-
`paramAt` says which call put which value there.
|
|
162
|
-
|
|
163
|
-
`callsInto` puts that together with the calls a function makes. A
|
|
164
|
-
project writes its own decorator that calls `Resolver()` and applies
|
|
165
|
-
that one to its classes, and the class is a resolver even though nothing
|
|
166
|
-
about it says `Resolver`. Getting several answers for one function is
|
|
167
|
-
normal rather than ambiguous, because a wrapper that combines two
|
|
168
|
-
library decorators applies both of them, so a caller asks whether the
|
|
169
|
-
one it cares about is among them.
|
|
170
|
-
|
|
171
|
-
Two relations exist for the rules' own use rather than for callers:
|
|
172
|
-
|
|
173
|
-
```
|
|
174
|
-
objectOf(x, obj) x stands for the object literal obj
|
|
175
|
-
invokes(r, f) the call r runs the function f
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
An object arrives two ways, through a name or as what a factory call
|
|
179
|
-
gives back. `objectOf` gives that step a name, so the rule for
|
|
180
|
-
`routes.list` and the rule for `make(body).handle` are the same rule.
|
|
181
|
-
This is where the two directions meet: a factory call gets an
|
|
182
|
-
`objectOf` answer without getting a `comesTo` answer.
|
|
183
|
-
|
|
184
|
-
`invokes` is the callee half of `givesBack`, kept apart so the case
|
|
185
|
-
where the callee is itself a call, `daoBuilder()()`, is one rule rather
|
|
186
|
-
than a copy of every other. It differs from `callsFunction`, which
|
|
187
|
-
starts from the function because a caller asking for call sites has the
|
|
188
|
-
function in hand.
|
|
189
|
-
|
|
190
|
-
## The anchor behind a receiver
|
|
191
|
-
|
|
192
|
-
A pack sometimes wants a call handed back, not a yes or no. Mongoose is
|
|
193
|
-
the picture: `model("User", schema)` is the anchor, and the pack reads
|
|
194
|
-
the model name and the collection off that call's own arguments. The
|
|
195
|
-
receiver in front of a matched method can be the model, a construction
|
|
196
|
-
of it (`new User({...})`), or a document a query returned
|
|
197
|
-
(`await User.findById(id)`), and each is a different number of hops
|
|
198
|
-
from the anchor.
|
|
199
|
-
|
|
200
|
-
`anchorChain` is the reachability that covers all three. From an asked
|
|
201
|
-
value (`wantedAnchor`) it follows names (`binds`, `endsHolding`,
|
|
202
|
-
`fallbackBranch`), imports through the export table, a call to its
|
|
203
|
-
callee, and a method's callee to its receiver. Every call the chain
|
|
204
|
-
passes lands in `wantedAnchorCall`, keyed by the asked value.
|
|
205
|
-
|
|
206
|
-
Candidates, not one answer: the chain has no way to rank a nearer call
|
|
207
|
-
above a farther one, and ranking inside the rules would be a depth
|
|
208
|
-
bound in a new spelling. The asking side filters the candidates against
|
|
209
|
-
its own origin (which module, which callee name) and applies the
|
|
210
|
-
single-answer policy: exactly one distinct match is the anchor, none or
|
|
211
|
-
several is a refusal.
|
|
212
|
-
|
|
213
|
-
The demand cone stays on the base facts named above. Nothing here pulls
|
|
214
|
-
`reaches` or `callsInto`, so asking about a receiver does not price in
|
|
215
|
-
call-graph closure.
|
|
216
|
-
|
|
217
|
-
## The subject behind a registration
|
|
218
|
-
|
|
219
|
-
Discovery keeps asking one question: is this receiver the app, the
|
|
220
|
-
router, the thing built by calling what a library exports? Which
|
|
221
|
-
`.get(...)` calls are routes and which `.use(...)` calls are mounts
|
|
222
|
-
both hang on the answer, and every language feature that moves a value
|
|
223
|
-
is a place the receiver can be written: a class field, a destructured
|
|
224
|
-
name, a property on an object another file built. Enumerating those
|
|
225
|
-
spellings in a walker loses to the language, so the question is asked
|
|
226
|
-
of the rules instead.
|
|
227
|
-
|
|
228
|
-
The answer comes back in two relations, both seeded by
|
|
229
|
-
`wantedSubject`.
|
|
230
|
-
`wantedSubjectWritten` is the written-value walk from the asked
|
|
231
|
-
receiver, so the asking side can apply the single-answer policy over
|
|
232
|
-
everything the receiver could be. `wantedSubjectConstruction` keeps
|
|
233
|
-
only the answers that are a call or a `new`, and pairs each with where
|
|
234
|
-
its callee was imported from, through however many aliases the callee
|
|
235
|
-
went. The asking side checks that pair against the (module, name) a
|
|
236
|
-
pack declares: exactly one distinct construction with a matching
|
|
237
|
-
origin is the subject, none or several is a refusal.
|
|
238
|
-
|
|
239
|
-
The construction end needs no fact of its own. Every call is already a
|
|
240
|
-
`call` fact, a `new` expression included, and the join against
|
|
241
|
-
`comesFrom` is what makes one of them a subject seed.
|
|
242
|
-
|
|
243
|
-
A call is written as itself, the same base case that makes a class its
|
|
244
|
-
own ancestor at the top of the chain. So when an adapter asks
|
|
245
|
-
`wantedSubjectWritten` about a call directly, the answers include the
|
|
246
|
-
call itself next to whatever the walk reaches. A call with one other
|
|
247
|
-
answer would count as two and be refused as ambiguous. `singleAnswers`
|
|
248
|
-
drops the row whose answer is its own key before counting, and every
|
|
249
|
-
adapter reads the relation through it.
|
|
250
|
-
|
|
251
|
-
A name written as a placeholder before a guard fills it in, `_client =
|
|
252
|
-
None` at module level and `_client = make_client()` inside a getter, is
|
|
253
|
-
two answers the same way. An adapter marks the placeholder write with
|
|
254
|
-
`placeholderValue(x)` and passes those keys to `singleAnswers`, which
|
|
255
|
-
sets them aside whenever the key has another answer. A name written only
|
|
256
|
-
as a placeholder keeps that answer. The TypeScript store does the same
|
|
257
|
-
for `null` and `undefined` in `sameConstructionAcrossWrites`.
|
|
258
|
-
|
|
259
|
-
A name bound to a call is a separate case. Its one answer is the call,
|
|
260
|
-
and the rule that says a call is written as what its callee returns
|
|
261
|
-
only fires for the call itself, never for a name that reaches one. An
|
|
262
|
-
adapter that wants the deeper answer asks `wantedSubject` a second
|
|
263
|
-
time with the call as the subject and takes that answer. The
|
|
264
|
-
TypeScript store does the same in `resolveWrittenValue` and in
|
|
265
|
-
`subjectConstructionOf`, whenever a name resolves to a call.
|
|
266
|
-
|
|
267
|
-
## Explaining an answer
|
|
268
|
-
|
|
269
|
-
Evaluate the same rules under `@suss/datalog`'s `witnesses` algebra and
|
|
270
|
-
every derived fact keeps the rule that fired and the facts it consumed,
|
|
271
|
-
so `proofOf` can rebuild the derivation tree of any answer on demand.
|
|
272
|
-
`explainResolutionProof` flattens that tree into the chain a person
|
|
273
|
-
reads: the step rules are the hops, `reaches` is the glue between them,
|
|
274
|
-
and the base cases ("x is already a function") end a chain without
|
|
275
|
-
adding to it. Each hop comes back with one sentence saying why it is
|
|
276
|
-
true, written from the rule's own name: `alias`, `import`, `argument`,
|
|
277
|
-
`factory unwrap`, and so on.
|
|
278
|
-
|
|
279
|
-
Two kinds of detail ride under a hop rather than beside it. A barrel
|
|
280
|
-
chain under an `import` hop lists which files forwarded the name. A
|
|
281
|
-
`declared wrapper` hop rests on a pack's word rather than on source, so
|
|
282
|
-
it surfaces as an assumption: "a pack declares that withSentry from
|
|
283
|
-
@sentry/serverless passes argument 0 through to its result". A proof
|
|
284
|
-
cut short by the depth cap says so instead of trailing off.
|
|
285
|
-
|
|
286
|
-
Atoms in a proof are whatever node ids the adapter interned, so both
|
|
287
|
-
functions take a `describe` callback that says an atom in source terms.
|
|
288
|
-
`renderExplanation` turns the flattened chain into printable lines;
|
|
289
|
-
`suss ask 'why does … reach …'` is this pipeline end to end.
|
|
290
|
-
|
|
291
|
-
## Why rules and not a walker
|
|
292
|
-
|
|
293
|
-
Each rule describes one hop. The chains people write are longer than
|
|
294
|
-
that, and nobody writes a rule for them: a factory handing off to
|
|
295
|
-
another factory, a closure three levels down calling the argument, a
|
|
296
|
-
barrel re-exporting a wrapper. The engine composes what it has, so all
|
|
297
|
-
of those work without anyone writing a rule for them.
|
|
298
|
-
|
|
299
|
-
That tells you where to look when something comes back empty. Suspect
|
|
300
|
-
the facts before the rules. Against one production service, suss
|
|
301
|
-
resolved 11 handlers and missed most of what the template declared. The
|
|
302
|
-
fix was one condition in fact extraction, and the existing rules found
|
|
303
|
-
the rest without any change.
|
|
304
|
-
|
|
305
|
-
## Changing a rule
|
|
306
|
-
|
|
307
|
-
Every rule here composes with every other one, so a change meant for one
|
|
308
|
-
construct lands on chains nobody had in mind. `@suss/resolution-fuzz`
|
|
309
|
-
generates four thousand fact bases, runs these rules over each, and
|
|
310
|
-
compares what came out against a committed baseline, which is what turns
|
|
311
|
-
a change into a diff a reviewer reads. Run `npm run resolution:baseline`
|
|
312
|
-
to accept one, and commit the rewritten file with the rule.
|
|
313
|
-
|
|
314
|
-
## What is not modelled
|
|
315
|
-
|
|
316
|
-
**A handler passed as one property of a config.** `make({ body:
|
|
317
|
-
handler })`, where the factory reads `opts.body`, does not resolve. We
|
|
318
|
-
tried a rule for it and took it back out. A wrapper often reads several
|
|
319
|
-
callbacks off the same config object, the rule made each one a
|
|
320
|
-
candidate for the whole call, and the ambiguity that produced nulled out
|
|
321
|
-
handlers that used to resolve. Working out which property is the handler
|
|
322
|
-
needs something the structure does not tell you: either "the only
|
|
323
|
-
property that gets called" (which needs negation) or a pack that says
|
|
324
|
-
which property it is. Until one of those exists, this pattern stays
|
|
325
|
-
unresolved on purpose.
|
|
326
|
-
|
|
327
|
-
**An element of an array.** `all[0]` has no fact for what an array
|
|
328
|
-
contains.
|
|
329
|
-
|
|
330
|
-
**Which write a read sees, once control flow decides it.** A name
|
|
331
|
-
written twice in a module's own statement list does resolve. Those
|
|
332
|
-
statements run once each, top to bottom, so the last write is what
|
|
333
|
-
anything importing the name gets, and the adapter says so with
|
|
334
|
-
`endsHolding`. A write inside a branch, a loop, or a function body is a
|
|
335
|
-
different claim, and the adapter says nothing about it. The name then
|
|
336
|
-
resolves to nothing, and it stays that way until we have control-flow
|
|
337
|
-
facts to reason over.
|
|
338
|
-
|
|
339
|
-
Answering that in general is reaching definitions, per use rather than
|
|
340
|
-
per name. That needs facts saying which statement follows which and
|
|
341
|
-
which branch each one is in, and no adapter emits any of those today.
|
|
342
|
-
|
|
343
|
-
We have the adapter pick the write rather than a rule, and that is a
|
|
344
|
-
cost decision. Ordering writes inside the rules means asking which
|
|
345
|
-
writes have no later write. Negation says that in one line, but this
|
|
346
|
-
evaluator has to throw its last fixpoint away and start over whenever a
|
|
347
|
-
rule set uses negation. The store evaluates after every wave of facts,
|
|
348
|
-
so one negated rule turned a 66 second run on the Saleor dashboard into
|
|
349
|
-
one that had not finished in ten minutes. Source order is something
|
|
350
|
-
every adapter already knows.
|
|
21
|
+
## More
|
|
351
22
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
23
|
+
- [How the rules follow a value](./DESIGN.md)
|
|
24
|
+
- [Documentation](https://nimbuscloud-ai.github.io/suss/)
|
|
25
|
+
- [Source and issues](https://github.com/nimbuscloud-ai/suss)
|
|
355
26
|
|
|
356
|
-
|
|
27
|
+

|
|
357
28
|
|
|
358
|
-
|
|
29
|
+
Apache 2.0. See [LICENSE](../../LICENSE).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@suss/resolution",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "The rules suss uses to follow a name in TypeScript, Python or Ruby to the function it comes down to.",
|
|
6
6
|
"keywords": [
|
|
@@ -56,6 +56,6 @@
|
|
|
56
56
|
"access": "public"
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
|
-
"@suss/datalog": "0.
|
|
59
|
+
"@suss/datalog": "0.24.0"
|
|
60
60
|
}
|
|
61
61
|
}
|