@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 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.