Skip to content

Add resolution diagnostic for declaration kind mismatch#435

Closed
Morriar wants to merge 3 commits intomainfrom
at-add-declaration-kind
Closed

Add resolution diagnostic for declaration kind mismatch#435
Morriar wants to merge 3 commits intomainfrom
at-add-declaration-kind

Conversation

@Morriar
Copy link
Contributor

@Morriar Morriar commented Jan 9, 2026

One more step towards #354.

This required a way to assert on the kind of the declaration. I added a DeclarationKind enum than can be used to both build a new declaration (which removes the need for closures in the resolution) and know what kind an existing declaration is.

Another subtlety is to which definition location to attach the error. Because declarations are created in a specific order that may not match their order in the code, we need to add some logic so the error is displayed on the last introduced one rather than the first one.

This PR is easier to review commit by commit.

@Morriar Morriar self-assigned this Jan 9, 2026
@Morriar Morriar requested a review from a team as a code owner January 9, 2026 16:21
@Morriar Morriar mentioned this pull request Jan 9, 2026
7 tasks
@Morriar Morriar force-pushed the at-test-resolution-diags branch from 9d42ee6 to 54b88b9 Compare January 9, 2026 16:22
@Morriar Morriar force-pushed the at-add-declaration-kind branch from 269165b to f8e0a7a Compare January 9, 2026 16:22
@Morriar Morriar force-pushed the at-test-resolution-diags branch from 54b88b9 to a7d71a2 Compare January 9, 2026 18:31
@Morriar Morriar force-pushed the at-add-declaration-kind branch from f8e0a7a to 2c28261 Compare January 9, 2026 18:31
&self.diagnostics
}

pub fn add_diagnostic(&mut self, diagnostics: Diagnostics, uri_id: UriId, offset: Offset, message: String) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not used?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Introduced too early, I can remove it from here 👍

(3004, TopLevelMixinSelf, Severity::Warning);

// 4000 - Resolution errors
(4001, KindRedefinition, Severity::Error);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean, if a library of my project has

# a fixture file bundled in the gem
# not required by my project
class User; end

And in my project, I define

module User
  class Something; end
end

I'll then get an error always show up in Ruby LSP?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, which should be the same with Sorbet until you ignore the fixtures directory.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But unlike Sorbet, rubydex under the usage of Ruby LSP will index dependencies, regardless whether they'd be required or not (which is different then gem rbi Tapioca generates through runtime). So in this case the chance of having this error will be much higher. And when that happens, users will need to exclude specific gem file/folder, which is harder to configure.
I think we should be more conservative on showing this as errors. IMO warning would be better.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My 2 cents: I think this should be an error. We only index gems' require paths. If a gem includes a fixture as part of its require path with code that conflicts with the real gem's implementation, I would argue that's a mistake in the gem.

Re-defining a class/module type crashes in the runtime, so I would stick with error.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I didn't know we only index the gem's require path. Where is this implemented?
If that's the case, then I'm good with this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This exact line

spec.require_paths.map { |path| File.join(spec.full_gem_path, path) }

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about cases like the recent RDoc patch? If we introduce Struct as a new type of definition, wouldn't that mean RDoc would always trigger this error?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two parts to the answer here. For Struct.new, it returns a class. We just need special handling for it, which I'm working on #392.

The second aspect I realized is that we actually need to be able to promote declarations since we can't control what kind of meta-programming a gem or project might be using. For example:

# Create a class via meta-programming. Rubydex has no idea and thinks this is
# a constant
Foo = meta_programming_class do
  # ....
end

# Then re-open the class and perform operations that are only valid in a namespace
# context, like including a module
class Foo
  include Bar
end

At the moment, we would be crashing in this case, so I have a branch where I'm exploring automatic promotion of declarations. Basically, if we find something that was a constant and then gets re-opened as a class, the most correct behaviour might be to promote it.

However, we can't allow redefinition from a class to a module. That's always an error.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@st0012 raised interesting questions and I think we can do better, please take a look at #450.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I rebased this work using the rules from #450.

@Morriar Morriar force-pushed the at-test-resolution-diags branch 3 times, most recently from ef3d51d to dc89e79 Compare January 14, 2026 15:13
Base automatically changed from at-test-resolution-diags to main January 14, 2026 16:29
Signed-off-by: Alexandre Terrasa <alexandre.terrasa@shopify.com>
Signed-off-by: Alexandre Terrasa <alexandre.terrasa@shopify.com>
@Morriar Morriar force-pushed the at-add-declaration-kind branch from 2c28261 to a569db6 Compare January 14, 2026 17:05
@Morriar Morriar requested review from st0012 and vinistock January 14, 2026 17:06
Signed-off-by: Alexandre Terrasa <alexandre.terrasa@shopify.com>
];

// We want to show the diagnostic for the most recent definition, so we sort by uri and offset
definitions.sort_by_key(|(d, _)| {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering we can have definitions across files that we cannot know the load order, does sorting really make a difference?

I'd say we can pick one to add the diagnostic and then we use related information to associate the diagnostic with all other definitions.

handle_constant_declaration(graph, *class.name_id(), id, false, |name, owner_id| {
Declaration::Class(Box::new(ClassDeclaration::new(name, owner_id)))
})
handle_constant_declaration(graph, *class.name_id(), &DeclarationKind::Class, id, false)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This refactor will conflict with your other refactor idea that requires enqueueing an ancestor linearization unit only when the closure to produce a new declaration gets invoked.

It might be worth splitting this refactor and considering it as part of the other one.

@Morriar
Copy link
Contributor Author

Morriar commented Jan 16, 2026

Closing in favor of #474.

@Morriar Morriar closed this Jan 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants