@suss/resolution 0.23.0 → 0.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +8 -337
  2. 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
- ## The facts an adapter supplies
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
- **Ambiguity is the caller's problem.** When the rules reach two
353
- different functions, the store returns nothing, because picking one
354
- would make the answer depend on the order the facts arrived in.
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
- ## License
27
+ ![coverage](../../.github/badges/coverage-resolution.svg)
357
28
 
358
- Licensed under Apache 2.0. See [LICENSE](../../LICENSE).
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.23.0",
3
+ "version": "0.24.1",
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.23.0"
59
+ "@suss/datalog": "0.24.1"
60
60
  }
61
61
  }