Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
121 commits
Select commit Hold shift + click to select a range
8d6e43f
Model Rails.event, Rails.error and Process.clock_gettime
tobi Sep 29, 2026
610eb02
Type controller `params` as ActionController::Parameters
tobi Sep 29, 2026
cab8256
Abstract sig stubs are declarations, not spliced methods
tobi Sep 29, 2026
5d33e8a
Do not splice abstract sig methods into includers; carry sigs onto sp…
tobi Sep 29, 2026
e6403a7
Keep the type a T.let or a trailing `#: Type` declares
tobi Sep 29, 2026
c1a2c2f
Resolve declared class names lexically and read a bare Hash as a hash
tobi Sep 29, 2026
4d5e2bf
Read a sorbet sig that returns a generic class
tobi Sep 29, 2026
ce8bf20
Bind multi-class rescues as a union and resolve qualified nested clas…
tobi Sep 29, 2026
fc472f9
Resolve a bare class read lexically before falling back to suffix search
tobi Sep 29, 2026
bee42af
Type `session` as a Session object, not a Hash[String, String]
tobi Sep 29, 2026
1363891
Add ActiveSupport's Hash core_ext to the Hash method table
tobi Sep 29, 2026
b7a0b13
Add Regexp's class-side escape, quote, last_match and union
tobi Sep 29, 2026
7f4926f
Keep session reads nilable and add Rack request accessors
tobi Sep 29, 2026
c9420bf
Read a zero-argument `order` on a model as the association reader
tobi Sep 29, 2026
293e20e
Limit the zero-argument association reader to relation-builder names
tobi Sep 29, 2026
1397cf2
A top-level class beats a same-named constant declared in another class
tobi Sep 29, 2026
9f857ce
Read Klass[...] sig types as the class
tobi Oct 6, 2026
573917f
Pin bare class constants resolving through the lexical nesting first
tobi Sep 29, 2026
af81584
Type `new` in a class-side method as an instance of the receiving class
tobi Sep 29, 2026
0351948
Return untyped from an abstract `raise NotImplementedError` hook
tobi Sep 29, 2026
1533fb1
Fold an extended module's instance methods onto the class side
tobi Sep 29, 2026
e89dd2d
Link a model to its app-defined base model for method lookup
tobi Sep 29, 2026
5d7be18
Type serialize-d columns as the coder's object, not the storage text
tobi Sep 29, 2026
e5c7c32
Bind a model to the table its self.table_name names
tobi Sep 29, 2026
31b9ab8
Honor a model's own self.table_name_prefix
tobi Sep 29, 2026
1d9b5d3
Leave class-owned, Time and mixed-array operators to Ruby
tobi Sep 29, 2026
c07e7ea
Type [a, b].min/.max on an array literal as an element, not T?
tobi Sep 29, 2026
0cc7d2e
Narrow a local on present?/blank? guards
tobi Sep 29, 2026
8fe0958
Dispatch on a class-side new's instance as the defining class
tobi Sep 29, 2026
21ff53f
Read T.unsafe and receiver-line `#: as untyped` as untyped assertions
tobi Sep 29, 2026
8368555
Register the schema-reflection class methods every model inherits
tobi Sep 29, 2026
a54e322
Answer the Object and Module protocol for any explicit receiver
tobi Sep 29, 2026
602e62e
Bind a rescued gem error as itself and type the Exception surface
tobi Sep 29, 2026
26e7cce
Give Integer the Comparable, Numeric and Integer methods it inherits
tobi Sep 29, 2026
37a8309
Register the AbstractAdapter surface on the connection
tobi Sep 29, 2026
0afece6
Type a params element read as String | Array | Parameters | nil
tobi Sep 29, 2026
55dd4bc
Answer String#to_date/to_d/in_time_zone and as_json on String, Array,…
tobi Sep 29, 2026
f2e1229
Read Parameters as a Hash of param values, not of Strings
tobi Sep 29, 2026
a0a0a72
Answer Array#intersect? and Hash#all?
tobi Sep 29, 2026
f412798
Answer the String methods the catalog was missing
tobi Sep 29, 2026
47520ab
Add UploadedFile to the params value union and Parameters class config
tobi Sep 29, 2026
bb52f55
Keep Parameters class config off the Hash fallback
tobi Sep 29, 2026
8f98a7c
Model the routes DSL that composes routes: draw/load, concerns, app s…
tobi Sep 29, 2026
2d328d4
Recognize `self.include(routes.url_helpers)` as the route-helper surface
tobi Sep 29, 2026
bd19a45
Dispatch an unknown class-side call through the class's method_missing
tobi Sep 29, 2026
e3b12b4
Register helpers generated from a YAML table by a factory
tobi Sep 29, 2026
432a68a
Carry mixins an app adds to a framework class into its surface
tobi Sep 29, 2026
58449cf
Add the Request readers the app reaches for: key_generator, cookie salts
tobi Sep 29, 2026
25b3c09
Resolve a controller's or module's relative include through its nesting
tobi Sep 29, 2026
7b46010
Ingest a non-controller class under app/controllers as a plain class
tobi Sep 29, 2026
17929e8
Observe the inline-signature providers through a controller subclass
tobi Sep 29, 2026
0d8bb45
Type the constants Ruby defines on Float as Floats
tobi Sep 29, 2026
28ca49b
Accept any two numbers in arithmetic, not only Integer with Float
tobi Sep 29, 2026
e551923
Compare points in time across Time, Date and DateTime
tobi Sep 29, 2026
89f8b02
Answer self[:column] and read_attribute with the column's type
tobi Sep 29, 2026
e6c6f8b
Trust a parameter a signature declares untyped
tobi Sep 29, 2026
236baf1
Do not widen an ivar initialize assigns with nil
tobi Sep 29, 2026
e08bc75
Narrow a self attribute reader on a guard like a local
tobi Sep 29, 2026
1de29bc
Let a fully declared parameter type beat what one caller passed
tobi Sep 29, 2026
f895e20
Read singleton, bot, top, type variables, intersections and literals …
tobi Sep 29, 2026
beae990
Read the rest of Sorbet's sig type grammar
tobi Sep 29, 2026
106b15d
Read `T.proc` types and a sig's block parameter
tobi Sep 29, 2026
9b5e1f8
Read a trailing `#: as T` / `#: as !nil` on any expression
tobi Sep 29, 2026
49e9a92
Read `T.must` and `T.must_because` as `not_nil!`
tobi Sep 29, 2026
5af6789
Bind a declared block's parameter to its type, not to the block's sig…
tobi Sep 29, 2026
143d10f
Drop a signature that would apply to both sides of a shared name
tobi Sep 29, 2026
e20bbce
Match a declared parameter by name, else by position among positionals
tobi Sep 29, 2026
3e6fe8f
Expand enum declarations whose labels or values are constants
tobi Sep 29, 2026
a0b2285
Ingest `...` argument forwarding as a splatted rest and a passed block
tobi Sep 29, 2026
460d0de
Accept constants as multiple-assignment targets
tobi Sep 29, 2026
b6f2c3f
Carry any defined? operand into the marker call
tobi Sep 29, 2026
91864c8
Treat index_by(&:to_sym) over a label list as the identity mapping
tobi Sep 29, 2026
95a9e2b
Read locked gems' Tapioca RBIs as a typed boundary to their classes
tobi Sep 29, 2026
7723192
Stop a bare constant reaching app classes by last segment alone
tobi Sep 29, 2026
cde4571
Stop calling in-repo components and stdlib constants unmodelled gems
tobi Sep 29, 2026
ce77930
Merge a gem RBI's methods into classes the registry already knows
tobi Sep 29, 2026
abd7428
Attribute a constant to a gem only if its RBI accounts for it
tobi Sep 29, 2026
9b574b7
Read every statement of a model's `class << self` instead of aborting…
tobi Sep 29, 2026
a50058f
Skip synthesized nodes in the unresolved-type warning
tobi Sep 29, 2026
9c9698a
Give controllers a class side: `class << self` and `def self.x`
tobi Sep 29, 2026
ca690d0
Ingest `class << self` at expression position
tobi Sep 29, 2026
141edd0
Reach a bare constant through every ancestor and a mixin's own namespace
tobi Sep 29, 2026
d1d08e6
Type session deletion like a nilable session read
tobi Sep 29, 2026
5a398e1
Report unclaimed model class-body setters
tobi Sep 30, 2026
930bf0f
Enforce Rubydex source identity and executable constant boundaries
tobi Oct 2, 2026
1897780
Preserve Rails enum aliases and refuse unmodeled raw assignment reads
tobi Oct 2, 2026
4cbd478
Retain literal framework load-hook mixins and report remaining gaps
tobi Oct 2, 2026
d536945
Reconcile inference and ingest with current main APIs
tobi Oct 2, 2026
b2570ed
Exercise inherited gem attribution through a real unresolved source call
tobi Oct 2, 2026
e92e8ec
Pin repeated Concern include precedence in emitted Ruby
tobi Oct 4, 2026
88d78f3
Reconcile branch invariants with current main APIs
tobi Oct 4, 2026
ec9a28d
Align source lifecycle and constant resolution with main
tobi Oct 5, 2026
a57f2b4
Keep generated GraphQL constant provenance when borrowing field spans
tobi Oct 5, 2026
0e78903
Preserve main's defined? operands with emitted execution coverage
tobi Oct 5, 2026
06e0cfe
Preserve named refusals for unimplemented nil assertions
tobi Oct 6, 2026
843fccf
Withdraw registry-only APIs without executable runtime support
tobi Oct 6, 2026
23a3fca
Require receiver identity and modeled ownership before admitting calls
tobi Oct 6, 2026
a1c2391
Pin load-hook macro execution together with carried instance methods
tobi Oct 6, 2026
bba9531
Respect an overridden class-side `new` in shared Concern factories
tobi Oct 6, 2026
8615c0c
Pin emitted execution and ledger coverage for recently merged behavior
tobi Oct 6, 2026
87df588
Merge main into internal fixes
tobi Oct 6, 2026
113eb5f
Fix extra-language emit for HeaderStore typing
cursoragent Oct 6, 2026
b9ecec0
Coalesce String? into typed String lists for Kotlin/C#
cursoragent Oct 6, 2026
be18f8a
Fix remaining extra-language compare emit failures
cursoragent Oct 6, 2026
d5a5c9b
Fix kotlin form_with dup and elixir FORGERY_SLOT emit
cursoragent Oct 6, 2026
9aa3414
Fix elixir found carry, go CSRF call, CodeRabbit emit nits
cursoragent Oct 6, 2026
0980b8c
Fix extras native compile: elixir ?__loop, go Performed, AC imports
cursoragent Oct 6, 2026
a722336
Classify @ivar << as record mutation for Elixir rewrite
cursoragent Oct 6, 2026
bdfbf4a
Fix extras CSRF fail-closed and elixir unused attrs
cursoragent Oct 6, 2026
8b8d339
Merge upstream/main into contrib/internal-fixes
cursoragent Oct 6, 2026
654bafc
Address CodeRabbit review on PR 503; land analyzer CI fixes
cursoragent Oct 6, 2026
b85b0f6
Merge upstream/main into contrib/internal-fixes (#495 CSRF/extras)
cursoragent Oct 6, 2026
c94ccc4
Elixir emit: Hash int-key Map.put; const-array << / clear via Process
cursoragent Oct 6, 2026
e691b24
Guide: say Roundhouse, not "the fork", for enum refuse
cursoragent Oct 6, 2026
cff15ca
Preserve Module#< for class Consts after test lower retype
cursoragent Oct 6, 2026
98d54b3
Defer INDEX when it reads Sound::BUILTIN (qualified)
cursoragent Oct 6, 2026
315af3a
Fix CodeRabbit: temporal_kind docs and ascription claim Drop guard
cursoragent Oct 6, 2026
b62c010
Keep spliced DnsTestHelper consts bare through Value qualify
cursoragent Oct 6, 2026
d66278d
Seed class constants when retyping lowered test methods
cursoragent Oct 6, 2026
1219a5c
Keep foreign spliced consts bare; still qualify same-class Values
cursoragent Oct 6, 2026
9e0c71c
Don't strip qualified consts on leaf-name collision
cursoragent Oct 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/guide/check.md
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,10 @@ roundhouse does not model it)* means the receiver comes from a gem the
census lists as unknown. Skip these on a first read. They exist so the
error count above means "findings", not "shadows of gaps".

Unresolved source constants keep their error severity even when the
gem census identifies a likely owner: their emitted expression is a
refusal stub. Gem context is added without certifying runtime support.

**The survey report** — printed only with `--continue`: every construct
ingest skipped, bucketed by kind, most frequent first, with the files
it occurred in. This is roundhouse's own to-do list for your app.
Expand Down
5 changes: 5 additions & 0 deletions docs/guide/transpile.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,11 @@ rather than dropping the callback or evaluating its producer per item.
Existing literal blocks, local block variables and bound-method
references retain their target-specific support and limitations.

Roundhouse currently refuses generated enum `*_before_type_cast` readers.
Rails returns the original input after an unsaved assignment and stored data
after persistence; the compiler has only the stored enum slot. Negative enum
scopes are supported. Source-defined readers keep their ordinary behavior.

## Apps that aren't fully covered yet

By default, ingest is strict — the first construct roundhouse does not
Expand Down
16 changes: 14 additions & 2 deletions docs/pipeline/analyze.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,19 +77,31 @@ registries:
Roundhouse keeps compact answers keyed by source position and
releases the Rubydex graph before its typing passes. A read that
resolves to a class keeps the qualified class path in the IR, so the
read names the same class after ingest copies a concern method into
read names the same class or value after ingest copies a concern method into
another class.
`Analyzer::build_constant_registry` infers each constant's value
type and stores it under the Rubydex declaration ID of its source
definition. The ID comes from the source position, so a file-level
constant that ingest moves into a class keeps its top-level ID. The
rounds continue until no value changes, so a chain such as `B = A`
resolves at any depth. A cycle stays unresolved.
Source snapshots and drains must preserve paths, text and order;
release builds enforce this identity and reject late registrations.
Prepared answers are reused only for the same ordered source contents.
Generated constants borrowing a real span carry an explicit origin bit;
missing Ruby documents or missing source-position answers cannot use
that fallback. Normal ingest persists `source_index_required`, and
clearing its source table triggers `source_index_missing`. Standalone
hand-built IR apps with no sources use exact modeled dispatch instead;
this API mode does not certify source indexing.
Views and generated expressions have no Rubydex answer. A bare name
there uses the typed local scope. A qualified name uses only the
value declared at that full name.
- The analyzer reports an error for an unresolved source constant.
It does not select another class by a matching name suffix.
This includes executable library methods, parameter defaults and
constant initializers; an unresolved constant cannot check clean and
emit an unsupported raise. It does not select another class by a matching
name suffix.
`parse_module_constants` types literal constants in framework Ruby.

Other inference gaps leave `Ty::Var(n)` or RBS `Ty::Untyped`.
Expand Down
2 changes: 2 additions & 0 deletions runtime/ruby/active_support_ext.rb
Original file line number Diff line number Diff line change
Expand Up @@ -817,6 +817,7 @@ def self.underscore(text)
end

def self.humanize(text)
raise NoMethodError, "undefined method 'humanize' for nil" if text.nil?
s = text.to_s.tr("_", " ").lstrip
s = s[0, s.length - 3].to_s if s.end_with?(" id")
s = s.downcase
Expand All @@ -825,6 +826,7 @@ def self.humanize(text)
end

def self.titleize(text)
raise NoMethodError, "undefined method 'titleize' for nil" if text.nil?
s = humanize(underscore(text))
out = +""
n = s.length
Expand Down
4 changes: 2 additions & 2 deletions runtime/ruby/active_support_ext.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,6 @@ module ActiveSupport
def self.local_time: (Integer y, Integer mo, Integer d, Integer h, Integer mi, Integer s, Integer nsec) -> Time
def self.number_delimited: (Integer | Float number) -> String
def self.underscore: (String text) -> String
def self.humanize: (String text) -> String
def self.titleize: (String text) -> String
def self.humanize: (String? text) -> String
def self.titleize: (String? text) -> String
end
25 changes: 20 additions & 5 deletions src/analyze/attribution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ pub fn attribute_unknown_gems(diags: &mut [Diagnostic], app: &App) {
return Some((gem, Some(format!("matches the declared `{dsl}` surface in {}; runtime method availability is unverified", owner.0))));
}
}
match ancestry.as_ref()?.receiver_gem(recv_ty, &census) {
match ancestry.as_ref()?.receiver_gem(recv_ty, &census, &|gem, path| app.gem_boundary.declares_path(gem, path)) {
GemClaim::Known { gem, constant } => Some((gem, Some(format!(
"receiver ancestry reaches `{constant}`; method ownership is unverified"
)))),
Expand All @@ -363,7 +363,10 @@ pub fn attribute_unknown_gems(diags: &mut [Diagnostic], app: &App) {
DiagnosticKind::Unsupported { construct, detail, .. }
if construct.as_str() == "constant" =>
{
crate::gems::gem_owning_constant(&census, detail).map(|gem| (gem, None))
crate::gems::gem_owning_constant_with(&census, detail, &|gem, path| {
app.gem_boundary.declares_path(gem, path)
})
.map(|gem| (gem, None))
}
_ => None,
}
Expand All @@ -375,7 +378,16 @@ pub fn attribute_unknown_gems(diags: &mut [Diagnostic], app: &App) {
&d.kind,
DiagnosticKind::Unsupported { construct, .. } if construct.as_str() == "constant"
);
if (!eligible(&d.kind) && !unknown_constant) || d.severity == Severity::Info {
if unknown_constant {
// Attribution cannot certify a constant whose emitted body
// is a refusal stub. Keep severity/kind/span and add context only.
if let Some((gem, _)) = gem_for(d) {
let context = format!(" — unmodeled gem `{gem}`; emitted constant availability is unverified");
if !d.message.ends_with(&context) { d.message.push_str(&context); }
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
continue;
}
if !eligible(&d.kind) || d.severity == Severity::Info {
continue;
}
if let Some((gem, evidence)) = gem_for(d) {
Expand Down Expand Up @@ -592,7 +604,7 @@ mod tests {
}

#[test]
fn unknown_gem_constant_is_a_coverage_note_not_a_user_error() {
fn unknown_gem_constant_keeps_its_error_and_idempotent_context() {
let mut app = App::new();
app.gem_lock = Some(crate::gems::Lockfile::parse(
"GEM\n remote: https://rubygems.org/\n specs:\n acme-core (1.0.0)\n\nPLATFORMS\n ruby\n\nDEPENDENCIES\n acme-core\n",
Expand All @@ -602,9 +614,12 @@ mod tests {
Diagnostic::unsupported(Span::synthetic(), None, "other construct", "AcmeCore::Client"),
];
attribute_unknown_gems(&mut diags, &app);
assert_eq!(diags[0].severity, Severity::Info);
assert_eq!(diags[0].severity, Severity::Error);
assert!(diags[0].message.contains("acme-core"));
assert_eq!(diags[1].severity, Severity::Error);
let once = diags.clone();
attribute_unknown_gems(&mut diags, &app);
assert_eq!(diags, once);
}

#[test]
Expand Down
12 changes: 6 additions & 6 deletions src/analyze/attribution/gem_ancestry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
use std::collections::{BTreeMap, HashMap, HashSet};

use crate::app::App;
use crate::gems::{GemCensus, gems_owning_constant};
use crate::gems::{GemCensus, gems_owning_constant_with};
use crate::ident::{ClassId, Symbol};
use crate::ty::Ty;

Expand Down Expand Up @@ -355,16 +355,16 @@ impl GemAncestry {
}
}

pub(super) fn receiver_gem<'a>(&self, ty: &Ty, census: &'a GemCensus) -> GemClaim<'a> {
pub(super) fn receiver_gem<'a>(&self, ty: &Ty, census: &'a GemCensus, declares: &dyn Fn(&str, &str) -> Option<bool>) -> GemClaim<'a> {
match ty {
Ty::Class { id, .. } => self.class_gem(id, census),
Ty::Class { id, .. } => self.class_gem(id, census, declares),
Ty::Union { variants } => {
let mut claim = GemClaim::Absent;
let mut missing = false;
// Match nullable dispatch: nil is not a concrete receiver arm.
// Every other arm must identify the same single gem.
for variant in variants.iter().filter(|ty| !matches!(ty, Ty::Nil)) {
match self.receiver_gem(variant, census) {
match self.receiver_gem(variant, census, declares) {
GemClaim::Absent => missing = true,
GemClaim::Uncertain => return GemClaim::Uncertain,
GemClaim::Known { gem, constant } => {
Expand Down Expand Up @@ -394,7 +394,7 @@ impl GemAncestry {
}
}

fn class_gem<'a>(&self, id: &ClassId, census: &'a GemCensus) -> GemClaim<'a> {
fn class_gem<'a>(&self, id: &ClassId, census: &'a GemCensus, declares: &dyn Fn(&str, &str) -> Option<bool>) -> GemClaim<'a> {
let mut pending = vec![id.clone()];
let mut seen = HashSet::new();
let mut candidates = BTreeMap::new();
Expand All @@ -410,7 +410,7 @@ impl GemAncestry {
pending.push(target);
}
} else {
for gem in gems_owning_constant(census, id.0.as_str()) {
for gem in gems_owning_constant_with(census, id.0.as_str(), declares) {
candidates
.entry(gem)
.and_modify(|witness: &mut String| {
Expand Down
Loading
Loading