@suss/adapter-ruby 0.21.1 → 0.22.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.
- package/README.md +78 -2
- package/dist/index.cjs +2224 -519
- package/dist/index.d.cts +280 -8
- package/dist/index.d.ts +280 -8
- package/dist/index.js +1902 -174
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Ruby language adapter for suss. It parses source with tree-sitter (WASM), resolv
|
|
|
6
6
|
|
|
7
7
|
`@suss/adapter-ruby` is the Ruby language adapter, per [`docs/internal/facts-and-rules.md`](../../../docs/internal/facts-and-rules.md)'s Layer 1 contract: discover units, emit summaries in the shared IR, emit facts. It parses a file with `web-tree-sitter` and a vendored Ruby grammar (`grammar/tree-sitter-ruby.wasm`, no native build step), tracks class/module nesting to qualify a constant the way Ruby itself would resolve it lexically, and discovers graphql-ruby's class-based `field` DSL on a class extending a pack-configured base class. A `field :x, mutation: Mutations::Y` or `field :x, resolver: Queries::Z` reference is followed one hop: the referenced class's own file is located by Rails' constant-to-path convention and read for its declared return type. Discovered units become `RawCodeStructure` objects handed to `@suss/extractor`'s `assembleSummary`, the same assembly code the other adapters use.
|
|
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
11
|
## The method behind a field
|
|
12
12
|
|
|
@@ -152,9 +152,85 @@ ends with tells a read from a write, and the keywords along it become the
|
|
|
152
152
|
selector. `fields` comes back empty, and a call on anything that is not a
|
|
153
153
|
constant says nothing, since there is no class to ask about.
|
|
154
154
|
|
|
155
|
+
## What a file reads from the environment
|
|
156
|
+
|
|
157
|
+
`ENV` is part of the language core, so the adapter recognizes reads of it
|
|
158
|
+
itself, without a pack. Each read becomes the same `config-read` interaction
|
|
159
|
+
the TypeScript adapter emits for `process.env.X`, on the binding
|
|
160
|
+
`runtime-config`, spelled `ENV["X"]` whichever way the source wrote it. The
|
|
161
|
+
runtime-config checker pairs those against what a template declares for the
|
|
162
|
+
process the file runs in.
|
|
163
|
+
|
|
164
|
+
| Ruby | Recognized as | Defaulted |
|
|
165
|
+
| --- | --- | --- |
|
|
166
|
+
| `ENV["X"]`, `ENV['X']`, `::ENV["X"]` | a read of `X` | no |
|
|
167
|
+
| `ENV.fetch("X")` | a read of `X` | no |
|
|
168
|
+
| `ENV.fetch("X", "d")`, `ENV.fetch("X", nil)` | a read of `X` | yes |
|
|
169
|
+
| `ENV.fetch("X") { "d" }`, `ENV.fetch("X") do ... end` | a read of `X` | yes |
|
|
170
|
+
| any of these followed by `\|\|` (`ENV["X"] \|\| "d"`) | a read of `X` | yes |
|
|
171
|
+
| `ENV[name]`, `ENV.fetch("#{prefix}_X")`, `ENV[:X]` | nothing: the name is not a string literal | |
|
|
172
|
+
| `ENV["X"] = "1"`, `ENV.key?("X")`, `Settings::ENV["X"]` | nothing: a write, a membership test, or another constant | |
|
|
173
|
+
| `other \|\| ENV["X"]` | `X` not defaulted, since it is the chain's last resort | |
|
|
174
|
+
|
|
175
|
+
A read inside a method a pack discovers (a resolver method behind a GraphQL
|
|
176
|
+
field) goes on that unit's summary. A read in the file body, in a class or
|
|
177
|
+
module body, or in a block at those levels runs when the file loads, so it goes
|
|
178
|
+
on a `module-init` summary named after the file, one per file that has such a
|
|
179
|
+
read. A read inside a method no pack discovers, or inside a lambda, is reported
|
|
180
|
+
nowhere, because nothing says when it runs.
|
|
181
|
+
|
|
182
|
+
## What a file depends on in the project
|
|
183
|
+
|
|
184
|
+
Every summary has `metadata.moduleImports`, the project files this file depends
|
|
185
|
+
on, relative to the workspace root and sorted. Ruby has no import statement, so
|
|
186
|
+
the list comes from two places: a `require_relative` whose target is a file in
|
|
187
|
+
the run, and a constant the file references that another file in the run
|
|
188
|
+
defines (`Settings::REGION` puts the file defining `Settings` in the list). A
|
|
189
|
+
plain `require` is not followed, because where it loads from depends on the
|
|
190
|
+
load path at run time. A file that depends on nothing in the project gets an
|
|
191
|
+
empty list rather than no field, so a Lambda handler that only requires gems
|
|
192
|
+
still tells the checker that its closure is the handler file alone.
|
|
193
|
+
|
|
194
|
+
## What a field's resolver reaches
|
|
195
|
+
|
|
196
|
+
A field's resolver method calls project methods, and those call others. Each method the field reaches this way gets a summary of its own, of kind `library`, bound as `function-call` with `transport: "in-process"` and `recognition: "reachable"`, with the calls, environment reads and database work its own body does. Each invocation effect on a field or a reached method says which summary the call lands on, in `summary`, so a reader answering "what does this field reach" follows `summary` from one unit to the next and never has to match a name.
|
|
197
|
+
|
|
198
|
+
The walk starts at the resolver method behind every discovered field (the one the section above finds), and adds a `calls` fact for each call in a body it could follow, until the set stops growing. A method two actions both reach gets one summary. A call the walk could not follow is recorded once per callee on the summary of the body it is in, as an `unfollowedCall` gap saying why, unless the reason is one nothing could have done better with (a call into a gem, a call through a parameter that some caller passes a method by name into, or one with no declaration this reader could find).
|
|
199
|
+
|
|
200
|
+
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
|
+
|
|
202
|
+
| Written as | Followed to |
|
|
203
|
+
| --- | --- |
|
|
204
|
+
| `helper` or `self.helper`, called bare in a method | that method in the enclosing class's own ancestry |
|
|
205
|
+
| `helper`, when nothing in the enclosing ancestry defines it | `def helper` written outside any class, project-wide |
|
|
206
|
+
| `Service.new.method` | `method` in `Service`'s own ancestry |
|
|
207
|
+
| `Service.method` | `def self.method` written in `Service`'s own body |
|
|
208
|
+
| `register(method(:build_index))`, where `register(handler)` calls `handler.call` or `handler.()` | `build_index`, followed from wherever a caller in the run named it, through the parameter `register`'s own body calls |
|
|
209
|
+
|
|
210
|
+
A method passed by name into a call is followed one hop further than the call itself. `method(:build_index)`, written bare with no receiver, is Ruby's way of naming a method rather than calling it, and only that bare form is followed; a `self.method(:build_index)` written with an explicit receiver is not. The same reference works as an `&`-prefixed block argument, `register(&method(:build_index))`, and is numbered by its position among the call's arguments, same as any other argument: `&method(...)` occupies whatever slot it is written in, and a receiving method's own `&blk` parameter is counted at its own declared position among that method's parameters, so the two line up without a separate convention for the block slot. `handler.call` (with or without parentheses) and the `handler.()` shorthand both invoke a `Proc` or `Method` a parameter is bound to, and are recognized the same way; a plain block passed with `do...end` or `{ }`, and `yield`, are not, so a resolver that only ever receives its block that way still gets `unboundParameter` on the call, with no join to fill it.
|
|
211
|
+
|
|
212
|
+
Where it stops, and what the gap says:
|
|
213
|
+
|
|
214
|
+
| Written as | Reason |
|
|
215
|
+
| --- | --- |
|
|
216
|
+
| `obj.send(:method)`, `public_send`, `__send__` | a dynamic send this run does not follow |
|
|
217
|
+
| a method the project writes with `define_method` | a body this reader cannot see |
|
|
218
|
+
| a bare name two files each define at the top level | more than one possible source |
|
|
219
|
+
| `Rails.cache.delete`, a call into a class this run does not define | outside the run (no gap) |
|
|
220
|
+
| `user.orders`, a local variable, or an instance variable | the value could not be settled |
|
|
221
|
+
| `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
|
+
| `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
|
+
| `service_class.new.method` where `service_class` is not a constant | the value could not be settled |
|
|
224
|
+
|
|
225
|
+
Not followed yet: a method found only on a superclass past an unread ancestor, a callable read out of a variable, the body of a block passed to `define_method`, and `yield`.
|
|
226
|
+
|
|
155
227
|
## Where it fits in suss
|
|
156
228
|
|
|
157
|
-
Depends on `@suss/extractor` (for `RawCodeStructure` / `assembleSummary`), `@suss/behavioral-ir`, `@suss/datalog` (for the fact database), and `web-tree-sitter`. Framework packs under `packages/framework/*` (
|
|
229
|
+
Depends on `@suss/extractor` (for `RawCodeStructure` / `assembleSummary`), `@suss/behavioral-ir`, `@suss/datalog` (for the fact database), and `web-tree-sitter`. Framework packs under `packages/framework/*` (`@suss/framework-graphql-ruby` and `@suss/framework-rails`) consume its `RubyPack` contract; nothing in this package knows what any particular library's classes, DSL calls, or base classes are called beyond graphql-ruby's own `field` / `argument` / `type` verbs, which the discovery logic reads structurally rather than through pack configuration.
|
|
230
|
+
|
|
231
|
+
## Extraction cache
|
|
232
|
+
|
|
233
|
+
This adapter uses the extraction cache shared across every language adapter, from `@suss/extractor`. It keeps the cache beside the project root, at `.suss/cache/`, and supplies the walked `.rb` file list as the key's file set; it has no config file of its own to guard the entry the way the TypeScript adapter's tsconfig does. Reuse today is whole-run only: any file changing re-extracts the whole project instead of that one file. See the extractor package's README for the design.
|
|
158
234
|
|
|
159
235
|
## Grammar asset
|
|
160
236
|
|