@suss/adapter-ruby 0.22.2 → 0.22.3
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 +27 -2
- package/dist/index.cjs +3049 -2638
- package/dist/index.d.cts +54 -4
- package/dist/index.d.ts +54 -4
- package/dist/index.js +3064 -2647
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -8,12 +8,30 @@ Ruby language adapter for suss. It parses source with tree-sitter (WASM), resolv
|
|
|
8
8
|
|
|
9
9
|
The adapter also discovers a `controllerActions` pattern: a class whose ancestry reaches a pack-configured base is a Rails-shaped controller, and each of its own public instance methods is one of its actions, bound to whatever method and path the pack's `routeFor` gives it. `@suss/framework-rails` is the pack that reads `config/routes.rb` and supplies that callback; the adapter itself contains no Rails string. `require` is not resolved; class and module nesting is. A resolver's transitions are always empty (`branches: []`), since a graphql field does no path-engine work, and confidence is pinned low.
|
|
10
10
|
|
|
11
|
+
## What an action responds with
|
|
12
|
+
|
|
13
|
+
A controller action gets one branch per path it can respond on, so an action that writes `:created` down one arm and `:unprocessable_entity` down the other reports both, each gated on the test that arm took.
|
|
14
|
+
|
|
15
|
+
The calls that send a response come from `responseStatusCalls` on the `controllerActions` pattern. Each entry says a receiverless call the library gives an action for that, and where the call takes its status, either as a keyword or at a positional index; when a call is written with both, the keyword wins. An entry may also give the status that call sends when the action writes none, which is how Rails' `redirect_to` reports 302 while its `render` reports the controller default. `statusCodeNames` on the same pattern gives the number behind each name the library accepts where a number could go, which for Rails is Rack's symbol table. No call name and no status name appears in this package.
|
|
16
|
+
|
|
17
|
+
The walk hands every response call to the shared path engine as a terminal, and the statement each one is written in ends its path, because Rails raises on a second render. Two things follow from that. A response after one that already ran is not reported, and a path that reaches the end of the body, or that ends in a bare `return`, is the implicit render.
|
|
18
|
+
|
|
19
|
+
Each branch gets its own `statusCodeReading`, alongside the pattern's `defaultStatusCode` as the library default, and `assembleSummary` collapses the two the same way it does for a status a Flask route returns in a tuple:
|
|
20
|
+
|
|
21
|
+
- A call that writes no status leaves that branch's reading absent, and the summary claims the call's own default, or the pattern's when the call declares none.
|
|
22
|
+
- A status argument goes through the shared value evaluator, so a number, a name, and a local variable that settles on either all read the same way.
|
|
23
|
+
- A status argument that does not settle on a number, `params[:code]` or a name the pack does not declare, claims nothing on that branch and gets one gap saying so. The other branches are unaffected.
|
|
24
|
+
|
|
25
|
+
A branch also gets the calls that reach it. `guardsHoldOn` from `@suss/extractor` decides that, comparing what gates each call against what gates the branch, which is the same test the Python and TypeScript adapters apply.
|
|
26
|
+
|
|
11
27
|
## The method behind a field
|
|
12
28
|
|
|
13
29
|
Most fields in a graphql-ruby schema get their value from a method. A summary should say a field has nothing behind it only when the adapter looked and found no such method.
|
|
14
30
|
|
|
15
31
|
The library calls a field's method with `public_send`, so that method can be written anywhere in the class's ancestry. `ancestry.ts` walks the chain, finding each ancestor by the same constant-to-path convention that a `mutation:` or `resolver:` reference already uses. A plain field gets its value from a method of the same name somewhere along that chain. A wired field gets its value from the `resolve` method somewhere along the wired class's chain. graphql-ruby decides which method name a wired class uses, so the pack says which one in `resolverMethodName`. It also says where a project's chain ends: `ancestryRootClassNames` lists the library's own root classes, and reaching one means the walk got past everything the project defined.
|
|
16
32
|
|
|
33
|
+
A bare superclass name is looked up the way Ruby looks it up: each level of the enclosing nesting, innermost first, then the top level. `module Api; class UsersController < ApplicationController` means `Api::ApplicationController` when a file or the same file defines that, and `ApplicationController` otherwise. When nothing defines any of them, the chain keeps the bare name as an unread ancestor, so a configured base that has no file of its own still matches by the name the project wrote.
|
|
34
|
+
|
|
17
35
|
The walk produces the same order Ruby does. Ruby builds a class's ancestors as each `include` runs. It works out the included module's own chain first, inserts that chain as a unit, skips anything already in the ancestors, and never moves a module that an earlier include or the superclass already placed. Two concerns sharing a base give `[C, B, A, Base]` rather than `[C, B, Base, A]`, and a base that the superclass already mixes in stays after the superclass. So the walk builds the superclass chain first, filters every later step against what is already there, and expands each sibling include on its own rather than descending into it with the set the siblings share. `include A, B` mixes in B before A, while `include A` followed by `include B` puts B first, so the walk reads calls in source order and reads a single call's arguments backwards.
|
|
18
36
|
|
|
19
37
|
The tests check this behavior, and the linearization itself was compared against `Module#ancestors` in a running Ruby process for a diamond, a three-way diamond, a diamond crossing a superclass boundary, `prepend`, multi-argument `include`, a module included twice, and a nested module chain.
|
|
@@ -42,7 +60,9 @@ shared rather than written again here.
|
|
|
42
60
|
|
|
43
61
|
| Ruby | Lowers to |
|
|
44
62
|
| --- | --- |
|
|
45
|
-
| `if` / `elsif` / `else
|
|
63
|
+
| `if` / `elsif` / `else` | one `if` per test, with the elsif chain nested into the else arm |
|
|
64
|
+
| `unless` | the same `if`, with the two arms the other way around, so a body that runs when the test fails says so |
|
|
65
|
+
| `render :gone if expired?` and the `unless` spelling of it | one `if` with a single arm, on whichever side of the test the modifier puts it |
|
|
46
66
|
| `while`, `until`, `for` | `loop` |
|
|
47
67
|
| a call with a `do` block, such as `items.each do \|i\|` | `loop`, because the block runs per iteration |
|
|
48
68
|
| `begin` / `rescue` / `ensure` | `try` |
|
|
@@ -199,9 +219,13 @@ The walk starts at the resolver method behind every discovered field (the one th
|
|
|
199
219
|
|
|
200
220
|
Ruby has no lexical binder for a local variable, so a callee is only followed when the source spells out where it goes: through the class ancestry `ancestry.ts` already computes, through a method the project writes outside any class, which Ruby mixes into every object as a private method, or through a method passed by name into the parameter that calls it.
|
|
201
221
|
|
|
222
|
+
A call written as a bare name, with no receiver, no arguments and no parentheses, is one of these. `visible_items` on its own parses as an identifier, the same node a local variable read parses as, so `bareCalls.ts` tells the two apart the way Ruby does: a name the method binds is a local variable, and every other identifier read is a call on self. A name is bound by a parameter, an assignment, a block or lambda parameter, a `for` variable, or a `rescue => err` clause. Binding is over-approximated on purpose: a name assigned anywhere in the method counts as a local even below the read, so the mistake this can make is missing a call rather than inventing one. An identifier written where a name is spelled rather than a value read, a method's own name or an assignment's left side, is left alone. So is one written as another call's receiver, since `orders.first` gives no way to resolve what `first` runs on.
|
|
223
|
+
|
|
224
|
+
A pack can also say which receiverless calls its own library defines, in `inheritedMethodNames` on the `controllerActions` pattern. A call by one of those names is left off the effect list and out of the reach walk, because an effect list is there to show what a body reaches in the project and nothing in the project defines those methods. The list applies to every body the run reads, not only to a discovered action, since the reach walk reaches methods that no pattern discovered. A call written against a receiver keeps its effect, so `page.render(json: 1)` is recorded even when a pack declared `render`. No such name appears in this package; `@suss/framework-rails` supplies Rails' own list.
|
|
225
|
+
|
|
202
226
|
| Written as | Followed to |
|
|
203
227
|
| --- | --- |
|
|
204
|
-
| `helper` or `self.helper`, called
|
|
228
|
+
| `helper`, `helper(x)` or `self.helper(x)`, called in a method | that method in the enclosing class's own ancestry |
|
|
205
229
|
| `helper`, when nothing in the enclosing ancestry defines it | `def helper` written outside any class, project-wide |
|
|
206
230
|
| `Service.new.method` | `method` in `Service`'s own ancestry |
|
|
207
231
|
| `Service.method` | `def self.method` written in `Service`'s own body |
|
|
@@ -218,6 +242,7 @@ Where it stops, and what the gap says:
|
|
|
218
242
|
| a bare name two files each define at the top level | more than one possible source |
|
|
219
243
|
| `Rails.cache.delete`, a call into a class this run does not define | outside the run (no gap) |
|
|
220
244
|
| `user.orders`, a local variable, or an instance variable | the value could not be settled |
|
|
245
|
+
| `visible_items.first`, where the receiver is itself a bare call | the value could not be settled |
|
|
221
246
|
| `handler.call` or `handler.()`, where `handler` is a parameter that some caller in the run passes a method by name into | followed through the join above (no gap) |
|
|
222
247
|
| `handler.call` or `handler.()`, where `handler` is a parameter that no caller in the run passes a method by name into | the caller supplies it, and nothing named what it passed |
|
|
223
248
|
| `service_class.new.method` where `service_class` is not a constant | the value could not be settled |
|