@suss/adapter-ruby 0.28.0 → 0.30.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/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
+ import { Database } from '@suss/datalog';
1
2
  import { Tree, Node } from 'web-tree-sitter';
2
3
  export { Node as RbNode, Tree as RbTree } from 'web-tree-sitter';
3
- import { Database } from '@suss/datalog';
4
4
  import { TypeShape, BehavioralSummary } from '@suss/behavioral-ir';
5
5
  import { RawCodeStructure, TimingReport, ExtractionReport, CacheDiagnostic, ExtractorOptions } from '@suss/extractor';
6
6
  import { WhyExplained } from '@suss/resolution';
@@ -38,6 +38,19 @@ declare function bodyStatements(body: Node): Node[];
38
38
  * included; the caller descends into that one itself.
39
39
  */
40
40
  declare function nestedStatements(stmt: Node): Node[];
41
+ /** What a pack says about one call whose block runs as part of the body around it, with the fields it left out settled. */
42
+ interface BodyBlockKind {
43
+ /** Whether the library only gives the call to a module, so a class writing the same name means something else. */
44
+ readonly moduleOnly: boolean;
45
+ /** Whether a `def` in the block declares a method on the class itself rather than on an instance. */
46
+ readonly definesClassMethods: boolean;
47
+ }
48
+ /**
49
+ * The calls a run's packs say run their block as part of the class or
50
+ * module body it is written in, by the name a project writes. Ruby
51
+ * defines no such call, so this is empty until a pack declares one.
52
+ */
53
+ type BodyBlocks = ReadonlyMap<string, BodyBlockKind>;
41
54
  /** A call whose block is the thing being configured rather than a place statements run. */
42
55
  type BlockConfigures = (call: Node) => boolean;
43
56
  /**
@@ -92,6 +105,33 @@ declare function underscoreConstantPath(qualifiedName: string, acronyms?: readon
92
105
  */
93
106
  declare function resolveConstantFile(root: string, qualifiedName: string, convention: ConstantPathConvention, acronyms?: readonly string[]): string | null;
94
107
 
108
+ /**
109
+ * The method names a class gets under a name the source computes.
110
+ *
111
+ * `KEYS.each { |key| define_method(key) { ... } }` defines one method
112
+ * per element of `KEYS`, and a reader of `def` nodes sees none of them.
113
+ * Reading the names lets a lookup for a name none of them defines carry
114
+ * on up the ancestry instead of stopping at the class.
115
+ *
116
+ * Which calls those are, and what each name is written as, arrive as
117
+ * `definesMethodFrom` and `nameTurnsOn`, so this reads no source of its
118
+ * own. Each name goes through the shared value evaluator, which follows
119
+ * names and constants across the whole run. A name read only in part
120
+ * becomes a pattern the whole name has to match.
121
+ */
122
+
123
+ /** What the dynamic definitions in one class define. */
124
+ interface DefinedNames {
125
+ /** Every name this reader read one as defining. */
126
+ readonly names: ReadonlySet<string>;
127
+ /** What a name read only in part has to match for the class to be defining it. */
128
+ readonly patterns: readonly RegExp[];
129
+ /** Whether one of them was given a name this reader could not read. */
130
+ readonly unreadable: boolean;
131
+ }
132
+ /** Every class's reading, by the key the value facts give the class node. */
133
+ type DynamicNames = ReadonlyMap<string, DefinedNames>;
134
+
95
135
  /**
96
136
  * Resolves Ruby constant paths against lexical nesting.
97
137
  *
@@ -174,6 +214,13 @@ type AncestorEntry = {
174
214
  };
175
215
  /** A class and everything it inherits from, in Ruby's own method-lookup order. */
176
216
  type Ancestry = readonly AncestorEntry[];
217
+ /** What reading a class body takes beyond the body: the run's facts, and what its packs declare about a block written in one. */
218
+ interface BodyReading {
219
+ readonly facts?: Database | undefined;
220
+ readonly bodyBlocks?: BodyBlocks | undefined;
221
+ /** What each class defines under a name the source computes, by class key. */
222
+ readonly dynamicNames?: DynamicNames | undefined;
223
+ }
177
224
  /** What a walk needs to reach a class it knows only by name. */
178
225
  interface AncestorLookup {
179
226
  /** Directory the constant-to-path convention resolves an ancestor's name against. */
@@ -214,7 +261,8 @@ type MethodLookup =
214
261
  * `cause` says why the search stopped, since a caller deciding
215
262
  * whether to look further needs more than the sentence: `unreadAncestor`
216
263
  * is a base whose file this run could not open, and `dynamicDefine`
217
- * is a body that calls `define_method`.
264
+ * is a `define_method` call that defines this name, or one whose own
265
+ * name this reader could not read.
218
266
  */
219
267
  | {
220
268
  type: "unsettled";
@@ -227,7 +275,7 @@ type MethodLookup =
227
275
  * The method `name` resolves to. An unread ancestor nearer than any
228
276
  * definition stops the search, since Ruby would have called that one first.
229
277
  */
230
- declare function methodInAncestry(ancestry: Ancestry, name: string): MethodLookup;
278
+ declare function methodInAncestry(ancestry: Ancestry, name: string, read?: BodyReading): MethodLookup;
231
279
  /** Every statement of every body reached, most distant ancestor first, so a nearer declaration overwrites what it inherits. */
232
280
  declare function inheritedStatements(ancestry: Ancestry, blockConfigures?: BlockConfigures): Array<{
233
281
  block: ReachedBody;
@@ -270,6 +318,40 @@ interface RubyPack {
270
318
  storage?: RbStoragePattern[];
271
319
  /** Calls the library gives a project for reading a model through a batching loader, rather than on the model itself. */
272
320
  loaders?: RbLoaderPattern[];
321
+ /** Calls the library gives a class or module body whose block runs as part of that body. */
322
+ bodyBlocks?: RbBodyBlock[];
323
+ /** What the project taught the library's inflector, which comes ahead of the adapter's own defaults. */
324
+ inflections?: RbInflections;
325
+ }
326
+ /**
327
+ * The words and rules a project registered with its library's
328
+ * inflector. The library says where a project writes them and how; what
329
+ * arrives here is the reading, so the adapter applies them without
330
+ * knowing any of that.
331
+ */
332
+ interface RbInflections {
333
+ /** Words a constant name keeps whole: `API` gives `APIToken` rather than `ApiToken`. */
334
+ acronyms?: string[];
335
+ /** A plural and the singular it comes from: `["people", "person"]`. */
336
+ irregular?: Array<[plural: string, singular: string]>;
337
+ /** Words spelled the same in both numbers. */
338
+ uncountable?: string[];
339
+ /** A rule turning a word singular, and what the part it matched becomes. A rule written `/body/flags` is a pattern; anything else is the text to replace. */
340
+ singular?: Array<[rule: string, replacement: string]>;
341
+ }
342
+ /**
343
+ * A receiverless call a library gives a class or module body, whose
344
+ * block runs as part of that body rather than keeping what it declares
345
+ * to itself. Ruby has no such call of its own, so a run whose packs
346
+ * declare nothing here never opens a block out into the body.
347
+ */
348
+ interface RbBodyBlock {
349
+ /** The call as a project writes it, `included`. */
350
+ name: string;
351
+ /** Set when the library only gives the call to a module, as a concern's own `included` is. */
352
+ moduleOnly?: boolean;
353
+ /** Set when a `def` in the block declares a method on the class itself rather than on an instance. */
354
+ definesClassMethods?: boolean;
273
355
  }
274
356
  /**
275
357
  * The calls a library gives a project for making a request. A method
@@ -321,15 +403,50 @@ interface RbClientResponse {
321
403
  * Ruby writes no return type, so a call is matched by what its receiver
322
404
  * inherits from. A model is a class that reaches one of these base classes,
323
405
  * whether the project declares an intermediate one or not.
406
+ *
407
+ * `reads` and `writes` are the whole of what the library does to the
408
+ * database. A method in neither list is the project's own or the
409
+ * language's, and the reach walk follows it instead.
324
410
  */
325
411
  interface RbStoragePattern {
326
412
  /** Base classes the library gives a model, `ActiveRecord::Base` for Rails. */
327
413
  baseClasses: string[];
328
- /** Methods that change what is stored. Anything else reads. */
414
+ /** Methods the library defines that change what is stored. */
329
415
  writes: string[];
416
+ /** Methods the library defines that run a query. */
417
+ reads: string[];
418
+ /** Methods whose result is the model again: one record, or a relation a later read narrows to one. */
419
+ givesBack: string[];
420
+ /** Methods that pick rows by the primary key when they are given a positional argument, and the column that is. */
421
+ byPrimaryKey?: RbPrimaryKeyLookup;
422
+ /** Read methods whose symbol arguments are the columns they ask for. */
423
+ columnArguments?: string[];
424
+ /** How the library declares that one model reaches another, when it has such a thing. */
425
+ associations?: RbAssociationCalls;
330
426
  /** Which database is behind the connection, which the project settles. */
331
427
  storageSystem: "postgresql" | "mysql" | "sqlite";
332
428
  }
429
+ /** Which methods take the primary key positionally, and what that column is called. */
430
+ interface RbPrimaryKeyLookup {
431
+ methods: string[];
432
+ /** The column the library uses unless a model says another. */
433
+ column: string;
434
+ }
435
+ /**
436
+ * The class-body calls a library gives a model for declaring an
437
+ * association, and where one says which class it reaches. The two lists
438
+ * are apart because the target's name is the association's singularised
439
+ * for a plural call and the association's as written for a singular one,
440
+ * and only the library knows which of its calls is which.
441
+ */
442
+ interface RbAssociationCalls {
443
+ /** Calls written in the singular, `belongs_to :account` and `has_one :profile`. */
444
+ singular: string[];
445
+ /** Calls written in the plural, `has_many :statuses`. */
446
+ plural: string[];
447
+ /** The keyword whose value is the target class's name instead, `class_name`. */
448
+ classNameKeyword: string;
449
+ }
333
450
  /**
334
451
  * A loader that batches reads of a model on the caller's behalf, graphql-ruby's
335
452
  * dataloader for one. The call that picks what to load is given the model as
@@ -486,10 +603,16 @@ interface GraphqlObjectFields {
486
603
  /**
487
604
  * storage.ts: which calls in a body talk to the database, for Ruby.
488
605
  *
489
- * A pack says which base class the library gives a model, and a call on a
490
- * class that reaches that base is a database call. The README says why
491
- * ancestry. A loader pattern from another pack adds calls that are given
492
- * the model as an argument instead, and those go through the same test.
606
+ * One rule covers both ways a call is written. It is database work when the
607
+ * pack lists its method as a read or a write, the class behind its receiver
608
+ * reaches a base class the pack lists, and the project does not declare that
609
+ * method itself. A chain counts once, at the outermost call the library
610
+ * defines, and every other call is left to the reach walk. The README says
611
+ * why ancestry.
612
+ *
613
+ * The class behind the receiver comes from the constant bindings for a chain
614
+ * written from a constant and from the resolution rules otherwise. A loader
615
+ * pattern adds calls given the model as an argument, tested the same way.
493
616
  */
494
617
 
495
618
  interface RbStorageOptions {
@@ -516,6 +639,10 @@ interface BodyReadOptions {
516
639
  readonly storage?: RbStorageOptions | undefined;
517
640
  /** The methods every pack in the run said its own library defines, which are left off an effect list. */
518
641
  readonly inheritedMethods?: InheritedMethods | undefined;
642
+ /** The calls every pack in the run said run their block as part of the body around it. */
643
+ readonly bodyBlocks?: BodyBlocks | undefined;
644
+ /** What each class defines under a name the source computes, by class key. */
645
+ readonly dynamicNames?: DynamicNames | undefined;
519
646
  }
520
647
  interface DiscoveryOptions extends BodyReadOptions {
521
648
  packs: RubyPack[];
@@ -540,6 +667,29 @@ interface ReachSeed {
540
667
  }
541
668
  declare function discoverUnits(root: Node, options: DiscoveryOptions): Promise<RawCodeStructure[]>;
542
669
 
670
+ /**
671
+ * associations.ts: reading `has_many :statuses` out of a model's body.
672
+ *
673
+ * A pack says which calls declare an association. The target is the
674
+ * class the `class_name:` keyword gives, or the association's own name
675
+ * put through ActiveSupport's default inflections when the declaration
676
+ * gives none. That inflected name is written nowhere in the source, so
677
+ * the declaration comes with a constant reference of its own and the
678
+ * constant bindings settle it from the nesting the model is written in:
679
+ * a `has_many :statuses` inside `Admin::Account` reaches `Admin::Status`
680
+ * before it reaches `Status`.
681
+ */
682
+
683
+ /** One association a class or module body declares. */
684
+ interface AssociationDeclaration {
685
+ /** The class or module whose body declares it, keyed the way a class is. */
686
+ readonly classKey: string;
687
+ /** The association's name, as the call writes it. */
688
+ readonly name: string;
689
+ /** The class it reaches, as a reference the constant bindings settle. */
690
+ readonly target: ConstantReference;
691
+ }
692
+
543
693
  /** A definition every file in the run can see, under the name it is written as. */
544
694
  interface ConstantDefinition {
545
695
  readonly qualifiedName: string;
@@ -559,12 +709,16 @@ interface FileConstants {
559
709
  readonly filePath: string;
560
710
  readonly definitions: readonly ConstantDefinition[];
561
711
  readonly references: readonly ConstantReference[];
712
+ /** Empty unless a pack in the run said what an association declaration looks like. */
713
+ readonly associations: readonly AssociationDeclaration[];
562
714
  }
563
715
  /**
564
716
  * Every constant this file defines and every one it reads. One walk, because
565
717
  * the nesting a reference is written inside is what the walk already knows.
718
+ * An association's target is read here too: the name it inflects to has no
719
+ * node in the source, and the nesting is what settles which class it means.
566
720
  */
567
- declare function collectFileConstants(filePath: string, root: Node): FileConstants;
721
+ declare function collectFileConstants(filePath: string, root: Node, associationCalls?: readonly RbAssociationCalls[], inflections?: RbInflections): FileConstants;
568
722
  /**
569
723
  * Link every constant read anywhere in the run to the definition behind it,
570
724
  * looking outwards from the nesting it was read inside the way Ruby does.
@@ -572,6 +726,10 @@ declare function collectFileConstants(filePath: string, root: Node): FileConstan
572
726
  * `binds(refKey, defKey)` is the link itself. `rbConstantFrom(from, to)`
573
727
  * is the same link at file level, which is the closest thing to an
574
728
  * import graph a language without imports has.
729
+ *
730
+ * `declaresAssociation` is emitted here as well, because an association's
731
+ * target is one of those references and the shared rules read the pair
732
+ * together.
575
733
  */
576
734
  declare function emitConstantBindings(db: Database, perFile: Iterable<FileConstants>): void;
577
735
 
@@ -590,7 +748,7 @@ declare function readKey(filePath: string, written: Node, enclosing: Node | null
590
748
  * Walk a file and emit the value facts. A method is walked in its own right,
591
749
  * so a body's returns and calls belong to the method that wrote them.
592
750
  */
593
- declare function emitValueFacts(db: Database, filePath: string, root: Node): void;
751
+ declare function emitValueFacts(db: Database, filePath: string, root: Node, bodyBlocks?: BodyBlocks): void;
594
752
 
595
753
  /**
596
754
  * The session behind `suss ask why` on a Ruby project: a tree-sitter
@@ -803,4 +961,4 @@ declare function methodDefinitionsIn(file: string, root: Node): Map<string, Node
803
961
  */
804
962
  declare const ADAPTER_VERSION = "0.1.0";
805
963
 
806
- export { ADAPTER_VERSION, type AncestorEntry, type AncestorLookup, type Ancestry, type CallArgs, type ClassInfo, type ConstantPathConvention, type ControllerActions, type DiscoveryOptions, type EvaluatedFile, type ExtractRubyOptions, type ExtractRubyResult, type FileCache, type FileConstants, type GraphqlObjectFields, type GraphqlTypeNameConvention, type MethodLookup, type ParameterBindings, type ProjectNodes, type Range, type RbStatusCall, type RbStoragePattern, type ReachedBody, type RubyDiscoveryPattern, type RubyExtendsSite, type RubyPack, type RubyRequireSite, type RubyStubEvidence, type RubyStubEvidenceOptions, type RubyValueHandle, RubyWhySession, type RubyWhySessionOptions, type TypeReadContext, ancestryOf, bindEvaluator, bodyStatements, booleanLiteralValue, collectFileConstants, createFileCache, discoverUnits, emitConstantBindings, emitEntryFact, emitValueFacts, evaluatedValue, extractRubyProject, field, findRubyFiles, graphqlTypeNameFromQualified, hashKeySymbolName, inheritedStatements, methodDefinitionsIn, methodInAncestry, nestedStatements, nodeId, parseRuby, parseRubySync, preloadRubyGrammar, qualifyConstantRef, reachDefinition, readCallArgs, readKey, resolveConstantFile, rubyStubEvidence, runStatements, stringLiteralValue, stringValueOf, symbolValue, typeShapeFromNode, underscoreConstantPath, unitKey, walkClasses, walkDefinitions };
964
+ export { ADAPTER_VERSION, type AncestorEntry, type AncestorLookup, type Ancestry, type CallArgs, type ClassInfo, type ConstantPathConvention, type ControllerActions, type DiscoveryOptions, type EvaluatedFile, type ExtractRubyOptions, type ExtractRubyResult, type FileCache, type FileConstants, type GraphqlObjectFields, type GraphqlTypeNameConvention, type MethodLookup, type ParameterBindings, type ProjectNodes, type Range, type RbBodyBlock, type RbInflections, type RbStatusCall, type RbStoragePattern, type ReachedBody, type RubyDiscoveryPattern, type RubyExtendsSite, type RubyPack, type RubyRequireSite, type RubyStubEvidence, type RubyStubEvidenceOptions, type RubyValueHandle, RubyWhySession, type RubyWhySessionOptions, type TypeReadContext, ancestryOf, bindEvaluator, bodyStatements, booleanLiteralValue, collectFileConstants, createFileCache, discoverUnits, emitConstantBindings, emitEntryFact, emitValueFacts, evaluatedValue, extractRubyProject, field, findRubyFiles, graphqlTypeNameFromQualified, hashKeySymbolName, inheritedStatements, methodDefinitionsIn, methodInAncestry, nestedStatements, nodeId, parseRuby, parseRubySync, preloadRubyGrammar, qualifyConstantRef, reachDefinition, readCallArgs, readKey, resolveConstantFile, rubyStubEvidence, runStatements, stringLiteralValue, stringValueOf, symbolValue, typeShapeFromNode, underscoreConstantPath, unitKey, walkClasses, walkDefinitions };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
+ import { Database } from '@suss/datalog';
1
2
  import { Tree, Node } from 'web-tree-sitter';
2
3
  export { Node as RbNode, Tree as RbTree } from 'web-tree-sitter';
3
- import { Database } from '@suss/datalog';
4
4
  import { TypeShape, BehavioralSummary } from '@suss/behavioral-ir';
5
5
  import { RawCodeStructure, TimingReport, ExtractionReport, CacheDiagnostic, ExtractorOptions } from '@suss/extractor';
6
6
  import { WhyExplained } from '@suss/resolution';
@@ -38,6 +38,19 @@ declare function bodyStatements(body: Node): Node[];
38
38
  * included; the caller descends into that one itself.
39
39
  */
40
40
  declare function nestedStatements(stmt: Node): Node[];
41
+ /** What a pack says about one call whose block runs as part of the body around it, with the fields it left out settled. */
42
+ interface BodyBlockKind {
43
+ /** Whether the library only gives the call to a module, so a class writing the same name means something else. */
44
+ readonly moduleOnly: boolean;
45
+ /** Whether a `def` in the block declares a method on the class itself rather than on an instance. */
46
+ readonly definesClassMethods: boolean;
47
+ }
48
+ /**
49
+ * The calls a run's packs say run their block as part of the class or
50
+ * module body it is written in, by the name a project writes. Ruby
51
+ * defines no such call, so this is empty until a pack declares one.
52
+ */
53
+ type BodyBlocks = ReadonlyMap<string, BodyBlockKind>;
41
54
  /** A call whose block is the thing being configured rather than a place statements run. */
42
55
  type BlockConfigures = (call: Node) => boolean;
43
56
  /**
@@ -92,6 +105,33 @@ declare function underscoreConstantPath(qualifiedName: string, acronyms?: readon
92
105
  */
93
106
  declare function resolveConstantFile(root: string, qualifiedName: string, convention: ConstantPathConvention, acronyms?: readonly string[]): string | null;
94
107
 
108
+ /**
109
+ * The method names a class gets under a name the source computes.
110
+ *
111
+ * `KEYS.each { |key| define_method(key) { ... } }` defines one method
112
+ * per element of `KEYS`, and a reader of `def` nodes sees none of them.
113
+ * Reading the names lets a lookup for a name none of them defines carry
114
+ * on up the ancestry instead of stopping at the class.
115
+ *
116
+ * Which calls those are, and what each name is written as, arrive as
117
+ * `definesMethodFrom` and `nameTurnsOn`, so this reads no source of its
118
+ * own. Each name goes through the shared value evaluator, which follows
119
+ * names and constants across the whole run. A name read only in part
120
+ * becomes a pattern the whole name has to match.
121
+ */
122
+
123
+ /** What the dynamic definitions in one class define. */
124
+ interface DefinedNames {
125
+ /** Every name this reader read one as defining. */
126
+ readonly names: ReadonlySet<string>;
127
+ /** What a name read only in part has to match for the class to be defining it. */
128
+ readonly patterns: readonly RegExp[];
129
+ /** Whether one of them was given a name this reader could not read. */
130
+ readonly unreadable: boolean;
131
+ }
132
+ /** Every class's reading, by the key the value facts give the class node. */
133
+ type DynamicNames = ReadonlyMap<string, DefinedNames>;
134
+
95
135
  /**
96
136
  * Resolves Ruby constant paths against lexical nesting.
97
137
  *
@@ -174,6 +214,13 @@ type AncestorEntry = {
174
214
  };
175
215
  /** A class and everything it inherits from, in Ruby's own method-lookup order. */
176
216
  type Ancestry = readonly AncestorEntry[];
217
+ /** What reading a class body takes beyond the body: the run's facts, and what its packs declare about a block written in one. */
218
+ interface BodyReading {
219
+ readonly facts?: Database | undefined;
220
+ readonly bodyBlocks?: BodyBlocks | undefined;
221
+ /** What each class defines under a name the source computes, by class key. */
222
+ readonly dynamicNames?: DynamicNames | undefined;
223
+ }
177
224
  /** What a walk needs to reach a class it knows only by name. */
178
225
  interface AncestorLookup {
179
226
  /** Directory the constant-to-path convention resolves an ancestor's name against. */
@@ -214,7 +261,8 @@ type MethodLookup =
214
261
  * `cause` says why the search stopped, since a caller deciding
215
262
  * whether to look further needs more than the sentence: `unreadAncestor`
216
263
  * is a base whose file this run could not open, and `dynamicDefine`
217
- * is a body that calls `define_method`.
264
+ * is a `define_method` call that defines this name, or one whose own
265
+ * name this reader could not read.
218
266
  */
219
267
  | {
220
268
  type: "unsettled";
@@ -227,7 +275,7 @@ type MethodLookup =
227
275
  * The method `name` resolves to. An unread ancestor nearer than any
228
276
  * definition stops the search, since Ruby would have called that one first.
229
277
  */
230
- declare function methodInAncestry(ancestry: Ancestry, name: string): MethodLookup;
278
+ declare function methodInAncestry(ancestry: Ancestry, name: string, read?: BodyReading): MethodLookup;
231
279
  /** Every statement of every body reached, most distant ancestor first, so a nearer declaration overwrites what it inherits. */
232
280
  declare function inheritedStatements(ancestry: Ancestry, blockConfigures?: BlockConfigures): Array<{
233
281
  block: ReachedBody;
@@ -270,6 +318,40 @@ interface RubyPack {
270
318
  storage?: RbStoragePattern[];
271
319
  /** Calls the library gives a project for reading a model through a batching loader, rather than on the model itself. */
272
320
  loaders?: RbLoaderPattern[];
321
+ /** Calls the library gives a class or module body whose block runs as part of that body. */
322
+ bodyBlocks?: RbBodyBlock[];
323
+ /** What the project taught the library's inflector, which comes ahead of the adapter's own defaults. */
324
+ inflections?: RbInflections;
325
+ }
326
+ /**
327
+ * The words and rules a project registered with its library's
328
+ * inflector. The library says where a project writes them and how; what
329
+ * arrives here is the reading, so the adapter applies them without
330
+ * knowing any of that.
331
+ */
332
+ interface RbInflections {
333
+ /** Words a constant name keeps whole: `API` gives `APIToken` rather than `ApiToken`. */
334
+ acronyms?: string[];
335
+ /** A plural and the singular it comes from: `["people", "person"]`. */
336
+ irregular?: Array<[plural: string, singular: string]>;
337
+ /** Words spelled the same in both numbers. */
338
+ uncountable?: string[];
339
+ /** A rule turning a word singular, and what the part it matched becomes. A rule written `/body/flags` is a pattern; anything else is the text to replace. */
340
+ singular?: Array<[rule: string, replacement: string]>;
341
+ }
342
+ /**
343
+ * A receiverless call a library gives a class or module body, whose
344
+ * block runs as part of that body rather than keeping what it declares
345
+ * to itself. Ruby has no such call of its own, so a run whose packs
346
+ * declare nothing here never opens a block out into the body.
347
+ */
348
+ interface RbBodyBlock {
349
+ /** The call as a project writes it, `included`. */
350
+ name: string;
351
+ /** Set when the library only gives the call to a module, as a concern's own `included` is. */
352
+ moduleOnly?: boolean;
353
+ /** Set when a `def` in the block declares a method on the class itself rather than on an instance. */
354
+ definesClassMethods?: boolean;
273
355
  }
274
356
  /**
275
357
  * The calls a library gives a project for making a request. A method
@@ -321,15 +403,50 @@ interface RbClientResponse {
321
403
  * Ruby writes no return type, so a call is matched by what its receiver
322
404
  * inherits from. A model is a class that reaches one of these base classes,
323
405
  * whether the project declares an intermediate one or not.
406
+ *
407
+ * `reads` and `writes` are the whole of what the library does to the
408
+ * database. A method in neither list is the project's own or the
409
+ * language's, and the reach walk follows it instead.
324
410
  */
325
411
  interface RbStoragePattern {
326
412
  /** Base classes the library gives a model, `ActiveRecord::Base` for Rails. */
327
413
  baseClasses: string[];
328
- /** Methods that change what is stored. Anything else reads. */
414
+ /** Methods the library defines that change what is stored. */
329
415
  writes: string[];
416
+ /** Methods the library defines that run a query. */
417
+ reads: string[];
418
+ /** Methods whose result is the model again: one record, or a relation a later read narrows to one. */
419
+ givesBack: string[];
420
+ /** Methods that pick rows by the primary key when they are given a positional argument, and the column that is. */
421
+ byPrimaryKey?: RbPrimaryKeyLookup;
422
+ /** Read methods whose symbol arguments are the columns they ask for. */
423
+ columnArguments?: string[];
424
+ /** How the library declares that one model reaches another, when it has such a thing. */
425
+ associations?: RbAssociationCalls;
330
426
  /** Which database is behind the connection, which the project settles. */
331
427
  storageSystem: "postgresql" | "mysql" | "sqlite";
332
428
  }
429
+ /** Which methods take the primary key positionally, and what that column is called. */
430
+ interface RbPrimaryKeyLookup {
431
+ methods: string[];
432
+ /** The column the library uses unless a model says another. */
433
+ column: string;
434
+ }
435
+ /**
436
+ * The class-body calls a library gives a model for declaring an
437
+ * association, and where one says which class it reaches. The two lists
438
+ * are apart because the target's name is the association's singularised
439
+ * for a plural call and the association's as written for a singular one,
440
+ * and only the library knows which of its calls is which.
441
+ */
442
+ interface RbAssociationCalls {
443
+ /** Calls written in the singular, `belongs_to :account` and `has_one :profile`. */
444
+ singular: string[];
445
+ /** Calls written in the plural, `has_many :statuses`. */
446
+ plural: string[];
447
+ /** The keyword whose value is the target class's name instead, `class_name`. */
448
+ classNameKeyword: string;
449
+ }
333
450
  /**
334
451
  * A loader that batches reads of a model on the caller's behalf, graphql-ruby's
335
452
  * dataloader for one. The call that picks what to load is given the model as
@@ -486,10 +603,16 @@ interface GraphqlObjectFields {
486
603
  /**
487
604
  * storage.ts: which calls in a body talk to the database, for Ruby.
488
605
  *
489
- * A pack says which base class the library gives a model, and a call on a
490
- * class that reaches that base is a database call. The README says why
491
- * ancestry. A loader pattern from another pack adds calls that are given
492
- * the model as an argument instead, and those go through the same test.
606
+ * One rule covers both ways a call is written. It is database work when the
607
+ * pack lists its method as a read or a write, the class behind its receiver
608
+ * reaches a base class the pack lists, and the project does not declare that
609
+ * method itself. A chain counts once, at the outermost call the library
610
+ * defines, and every other call is left to the reach walk. The README says
611
+ * why ancestry.
612
+ *
613
+ * The class behind the receiver comes from the constant bindings for a chain
614
+ * written from a constant and from the resolution rules otherwise. A loader
615
+ * pattern adds calls given the model as an argument, tested the same way.
493
616
  */
494
617
 
495
618
  interface RbStorageOptions {
@@ -516,6 +639,10 @@ interface BodyReadOptions {
516
639
  readonly storage?: RbStorageOptions | undefined;
517
640
  /** The methods every pack in the run said its own library defines, which are left off an effect list. */
518
641
  readonly inheritedMethods?: InheritedMethods | undefined;
642
+ /** The calls every pack in the run said run their block as part of the body around it. */
643
+ readonly bodyBlocks?: BodyBlocks | undefined;
644
+ /** What each class defines under a name the source computes, by class key. */
645
+ readonly dynamicNames?: DynamicNames | undefined;
519
646
  }
520
647
  interface DiscoveryOptions extends BodyReadOptions {
521
648
  packs: RubyPack[];
@@ -540,6 +667,29 @@ interface ReachSeed {
540
667
  }
541
668
  declare function discoverUnits(root: Node, options: DiscoveryOptions): Promise<RawCodeStructure[]>;
542
669
 
670
+ /**
671
+ * associations.ts: reading `has_many :statuses` out of a model's body.
672
+ *
673
+ * A pack says which calls declare an association. The target is the
674
+ * class the `class_name:` keyword gives, or the association's own name
675
+ * put through ActiveSupport's default inflections when the declaration
676
+ * gives none. That inflected name is written nowhere in the source, so
677
+ * the declaration comes with a constant reference of its own and the
678
+ * constant bindings settle it from the nesting the model is written in:
679
+ * a `has_many :statuses` inside `Admin::Account` reaches `Admin::Status`
680
+ * before it reaches `Status`.
681
+ */
682
+
683
+ /** One association a class or module body declares. */
684
+ interface AssociationDeclaration {
685
+ /** The class or module whose body declares it, keyed the way a class is. */
686
+ readonly classKey: string;
687
+ /** The association's name, as the call writes it. */
688
+ readonly name: string;
689
+ /** The class it reaches, as a reference the constant bindings settle. */
690
+ readonly target: ConstantReference;
691
+ }
692
+
543
693
  /** A definition every file in the run can see, under the name it is written as. */
544
694
  interface ConstantDefinition {
545
695
  readonly qualifiedName: string;
@@ -559,12 +709,16 @@ interface FileConstants {
559
709
  readonly filePath: string;
560
710
  readonly definitions: readonly ConstantDefinition[];
561
711
  readonly references: readonly ConstantReference[];
712
+ /** Empty unless a pack in the run said what an association declaration looks like. */
713
+ readonly associations: readonly AssociationDeclaration[];
562
714
  }
563
715
  /**
564
716
  * Every constant this file defines and every one it reads. One walk, because
565
717
  * the nesting a reference is written inside is what the walk already knows.
718
+ * An association's target is read here too: the name it inflects to has no
719
+ * node in the source, and the nesting is what settles which class it means.
566
720
  */
567
- declare function collectFileConstants(filePath: string, root: Node): FileConstants;
721
+ declare function collectFileConstants(filePath: string, root: Node, associationCalls?: readonly RbAssociationCalls[], inflections?: RbInflections): FileConstants;
568
722
  /**
569
723
  * Link every constant read anywhere in the run to the definition behind it,
570
724
  * looking outwards from the nesting it was read inside the way Ruby does.
@@ -572,6 +726,10 @@ declare function collectFileConstants(filePath: string, root: Node): FileConstan
572
726
  * `binds(refKey, defKey)` is the link itself. `rbConstantFrom(from, to)`
573
727
  * is the same link at file level, which is the closest thing to an
574
728
  * import graph a language without imports has.
729
+ *
730
+ * `declaresAssociation` is emitted here as well, because an association's
731
+ * target is one of those references and the shared rules read the pair
732
+ * together.
575
733
  */
576
734
  declare function emitConstantBindings(db: Database, perFile: Iterable<FileConstants>): void;
577
735
 
@@ -590,7 +748,7 @@ declare function readKey(filePath: string, written: Node, enclosing: Node | null
590
748
  * Walk a file and emit the value facts. A method is walked in its own right,
591
749
  * so a body's returns and calls belong to the method that wrote them.
592
750
  */
593
- declare function emitValueFacts(db: Database, filePath: string, root: Node): void;
751
+ declare function emitValueFacts(db: Database, filePath: string, root: Node, bodyBlocks?: BodyBlocks): void;
594
752
 
595
753
  /**
596
754
  * The session behind `suss ask why` on a Ruby project: a tree-sitter
@@ -803,4 +961,4 @@ declare function methodDefinitionsIn(file: string, root: Node): Map<string, Node
803
961
  */
804
962
  declare const ADAPTER_VERSION = "0.1.0";
805
963
 
806
- export { ADAPTER_VERSION, type AncestorEntry, type AncestorLookup, type Ancestry, type CallArgs, type ClassInfo, type ConstantPathConvention, type ControllerActions, type DiscoveryOptions, type EvaluatedFile, type ExtractRubyOptions, type ExtractRubyResult, type FileCache, type FileConstants, type GraphqlObjectFields, type GraphqlTypeNameConvention, type MethodLookup, type ParameterBindings, type ProjectNodes, type Range, type RbStatusCall, type RbStoragePattern, type ReachedBody, type RubyDiscoveryPattern, type RubyExtendsSite, type RubyPack, type RubyRequireSite, type RubyStubEvidence, type RubyStubEvidenceOptions, type RubyValueHandle, RubyWhySession, type RubyWhySessionOptions, type TypeReadContext, ancestryOf, bindEvaluator, bodyStatements, booleanLiteralValue, collectFileConstants, createFileCache, discoverUnits, emitConstantBindings, emitEntryFact, emitValueFacts, evaluatedValue, extractRubyProject, field, findRubyFiles, graphqlTypeNameFromQualified, hashKeySymbolName, inheritedStatements, methodDefinitionsIn, methodInAncestry, nestedStatements, nodeId, parseRuby, parseRubySync, preloadRubyGrammar, qualifyConstantRef, reachDefinition, readCallArgs, readKey, resolveConstantFile, rubyStubEvidence, runStatements, stringLiteralValue, stringValueOf, symbolValue, typeShapeFromNode, underscoreConstantPath, unitKey, walkClasses, walkDefinitions };
964
+ export { ADAPTER_VERSION, type AncestorEntry, type AncestorLookup, type Ancestry, type CallArgs, type ClassInfo, type ConstantPathConvention, type ControllerActions, type DiscoveryOptions, type EvaluatedFile, type ExtractRubyOptions, type ExtractRubyResult, type FileCache, type FileConstants, type GraphqlObjectFields, type GraphqlTypeNameConvention, type MethodLookup, type ParameterBindings, type ProjectNodes, type Range, type RbBodyBlock, type RbInflections, type RbStatusCall, type RbStoragePattern, type ReachedBody, type RubyDiscoveryPattern, type RubyExtendsSite, type RubyPack, type RubyRequireSite, type RubyStubEvidence, type RubyStubEvidenceOptions, type RubyValueHandle, RubyWhySession, type RubyWhySessionOptions, type TypeReadContext, ancestryOf, bindEvaluator, bodyStatements, booleanLiteralValue, collectFileConstants, createFileCache, discoverUnits, emitConstantBindings, emitEntryFact, emitValueFacts, evaluatedValue, extractRubyProject, field, findRubyFiles, graphqlTypeNameFromQualified, hashKeySymbolName, inheritedStatements, methodDefinitionsIn, methodInAncestry, nestedStatements, nodeId, parseRuby, parseRubySync, preloadRubyGrammar, qualifyConstantRef, reachDefinition, readCallArgs, readKey, resolveConstantFile, rubyStubEvidence, runStatements, stringLiteralValue, stringValueOf, symbolValue, typeShapeFromNode, underscoreConstantPath, unitKey, walkClasses, walkDefinitions };