@suss/adapter-ruby 0.3.2-0 → 0.5.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 +120 -0
- package/dist/index.cjs +16132 -134
- package/dist/index.d.cts +25 -3
- package/dist/index.d.ts +25 -3
- package/dist/index.js +16118 -114
- package/package.json +5 -4
package/README.md
CHANGED
|
@@ -32,6 +32,126 @@ What a body does still goes unread. Reading it needs the path engine: statements
|
|
|
32
32
|
|
|
33
33
|
One thing the walk deliberately leaves out. A `field` declared on a base object type is not discovered on its subclasses. That would change which units exist rather than what each one says about itself, and it is a separate piece of work.
|
|
34
34
|
|
|
35
|
+
## What a body lowers to
|
|
36
|
+
|
|
37
|
+
The adapter lowers a method body into the statement form the shared path
|
|
38
|
+
engine in `@suss/extractor` walks, the same engine the Python and TypeScript
|
|
39
|
+
adapters use. It is generic over the language's own condition handle and never
|
|
40
|
+
looks inside one, so the enumeration and the negation of an earlier arm are
|
|
41
|
+
shared rather than written again here.
|
|
42
|
+
|
|
43
|
+
| Ruby | Lowers to |
|
|
44
|
+
| --- | --- |
|
|
45
|
+
| `if` / `elsif` / `else`, `unless` | one `if` per test, with the elsif chain nested into the else arm |
|
|
46
|
+
| `while`, `until`, `for` | `loop` |
|
|
47
|
+
| a call with a `do` block, such as `items.each do \|i\|` | `loop`, because the block runs per iteration |
|
|
48
|
+
| `begin` / `rescue` / `ensure` | `try` |
|
|
49
|
+
| `case` / `when` / `else` | `switch`, with `else` as the default group |
|
|
50
|
+
| `return`, `raise`, `break`, `next` | `exit` |
|
|
51
|
+
| anything else | `opaque` |
|
|
52
|
+
|
|
53
|
+
Three things read differently from Python, and each one is why this file
|
|
54
|
+
exists rather than a shared lowering:
|
|
55
|
+
|
|
56
|
+
- **`raise` is an ordinary method call**, not a keyword, so a throw is
|
|
57
|
+
recognised by the call's name rather than by a node type.
|
|
58
|
+
- **A `return` inside a `do` block returns from the method**, so the scan
|
|
59
|
+
descends into one. A lambda captures its own return, so the scan stops
|
|
60
|
+
there.
|
|
61
|
+
- **A method returns its last expression** with no `return` written, which
|
|
62
|
+
Python has no equivalent of.
|
|
63
|
+
|
|
64
|
+
### Keying anything on a node
|
|
65
|
+
|
|
66
|
+
tree-sitter hands back a fresh wrapper object every time a child is read, so
|
|
67
|
+
two reads of one node are never `===` and a plain `Set` or `Map` keyed on a
|
|
68
|
+
node matches nothing. Use `NodeSet` and `NodeMap`, which key on the node id.
|
|
69
|
+
`npm run check:style` fails a build that keys either on a node.
|
|
70
|
+
|
|
71
|
+
## Finding the definition behind a constant
|
|
72
|
+
|
|
73
|
+
Ruby has no imports. A file says `require` to load another file, and after
|
|
74
|
+
that every constant either one defines is reachable by name, so which file a
|
|
75
|
+
name came from is settled by where the constant is defined rather than by
|
|
76
|
+
anything written at the reading site. The other two adapters emit `imports`
|
|
77
|
+
and this one binds a reference straight to its definition.
|
|
78
|
+
|
|
79
|
+
Lookup follows Ruby's own rule. A name written inside `module Types; class
|
|
80
|
+
Wrapper` is looked for as `Types::Wrapper::Order`, then `Types::Order`, then
|
|
81
|
+
`Order`, and the first one that settles wins:
|
|
82
|
+
|
|
83
|
+
```ruby
|
|
84
|
+
module Types
|
|
85
|
+
class Order; end
|
|
86
|
+
class Wrapper
|
|
87
|
+
def build
|
|
88
|
+
Order # Types::Order, not the top-level one
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
A name two files define under the same nesting says nothing, because choosing
|
|
95
|
+
between them would be a guess. Neither does a constant built at run time
|
|
96
|
+
through `const_set` or `Object.const_get`, which nothing here reads.
|
|
97
|
+
|
|
98
|
+
The definitions are collected per file and matched afterwards, since which
|
|
99
|
+
file defines a constant is only settled once every file has been read.
|
|
100
|
+
|
|
101
|
+
## What a class inherits
|
|
102
|
+
|
|
103
|
+
A class says which class it extends, twice. `extends` points at whatever the
|
|
104
|
+
superclass name binds to, so the shared rules find a method a base class
|
|
105
|
+
declares on a subclass that never overrode it. `extendsNamed` keeps the name
|
|
106
|
+
as written, because a base class the project does not declare, like
|
|
107
|
+
`ActiveRecord::Base`, has no node in the run to point at:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
class Order < ApplicationRecord
|
|
111
|
+
end
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
extends order.rb:0-31 order.rb#ApplicationRecord
|
|
116
|
+
extendsNamed order.rb:0-31 ApplicationRecord
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
A pack matching a library base class reads the second one, and follows the
|
|
120
|
+
first to keep going up.
|
|
121
|
+
|
|
122
|
+
## What a body does with the database
|
|
123
|
+
|
|
124
|
+
Ruby writes no return type, so the Python adapter's trick of reading what a
|
|
125
|
+
method says it gives back has no counterpart here. A pack says which base
|
|
126
|
+
class the library gives a model instead:
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
storage: [
|
|
130
|
+
{
|
|
131
|
+
baseClasses: ["ActiveRecord::Base"],
|
|
132
|
+
writes: ["update", "destroy", "save", "create", "delete_all"],
|
|
133
|
+
storageSystem: "postgres",
|
|
134
|
+
},
|
|
135
|
+
]
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
A call matches when the constant its receivers start at reaches one of those
|
|
139
|
+
base classes. Rails puts its own class in between, and following `extends`
|
|
140
|
+
through the project and matching `extendsNamed` at the library takes care of
|
|
141
|
+
that:
|
|
142
|
+
|
|
143
|
+
```ruby
|
|
144
|
+
class ApplicationRecord < ActiveRecord::Base; end
|
|
145
|
+
class Order < ApplicationRecord; end
|
|
146
|
+
|
|
147
|
+
Order.where(id: 1).first # one read, against Order, picking rows by id
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
A chain is one thing the code does, so that counts once. The method the chain
|
|
151
|
+
ends with tells a read from a write, and the keywords along it become the
|
|
152
|
+
selector. `fields` comes back empty, and a call on anything that is not a
|
|
153
|
+
constant says nothing, since there is no class to ask about.
|
|
154
|
+
|
|
35
155
|
## Where it fits in suss
|
|
36
156
|
|
|
37
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/*` (starting with `@suss/framework-graphql-ruby`) consume its `RubyPack` contract; nothing in this package knows what any particular library's classes or DSL calls are named beyond graphql-ruby's own `field` / `argument` / `type` verbs, which the discovery logic reads structurally rather than through pack configuration.
|