Skip to content

Commit

Permalink
Merge pull request soutaro#1274 from tk0miya/type_construction/Single…
Browse files Browse the repository at this point in the history
…tonTypeMismatch

Emit SingletonTypeMismatch when class/module mismatch
  • Loading branch information
soutaro authored Nov 25, 2024
2 parents aef81cf + e04e6f7 commit d7cb1f7
Show file tree
Hide file tree
Showing 5 changed files with 180 additions and 29 deletions.
30 changes: 30 additions & 0 deletions lib/steep/diagnostic/ruby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -409,6 +409,33 @@ def header_line
end
end

class ClassModuleMismatch < Base
attr_reader :name

def initialize(node:, name:)
case node.type
when :module, :class
location = node.loc.name # steep:ignore NoMethod
else
raise "Unexpected node: #{node.type}"
end
super(node: node, location: location) # steep:ignore NoMethod

@name = name
end

def header_line
case node&.type
when :module
"#{name} is declared as a class in RBS"
when :class
"#{name} is declared as a module in RBS"
else
raise
end
end
end

class MethodArityMismatch < Base
attr_reader :method_type

Expand Down Expand Up @@ -968,6 +995,7 @@ def self.default
ReturnTypeMismatch => :error,
SetterBodyTypeMismatch => :information,
SetterReturnTypeMismatch => :information,
ClassModuleMismatch => :error,
SyntaxError => :hint,
TypeArgumentMismatchError => :hint,
UnannotatedEmptyCollection => :warning,
Expand Down Expand Up @@ -1025,6 +1053,7 @@ def self.strict
ReturnTypeMismatch => :error,
SetterBodyTypeMismatch => :error,
SetterReturnTypeMismatch => :error,
ClassModuleMismatch => :error,
SyntaxError => :hint,
TypeArgumentMismatchError => :error,
UnannotatedEmptyCollection => :error,
Expand Down Expand Up @@ -1082,6 +1111,7 @@ def self.lenient
ReturnTypeMismatch => :warning,
SetterBodyTypeMismatch => nil,
SetterReturnTypeMismatch => nil,
ClassModuleMismatch => nil,
SyntaxError => :hint,
TypeArgumentMismatchError => nil,
UnannotatedEmptyCollection => :hint,
Expand Down
68 changes: 40 additions & 28 deletions lib/steep/type_construction.rb
Original file line number Diff line number Diff line change
Expand Up @@ -357,35 +357,40 @@ def for_module(node, new_module_name)
end

if implement_module_name
module_entry = checker.factory.definition_builder.env.normalized_module_entry(implement_module_name.name) or raise

module_context = module_context.update(
instance_type: AST::Types::Intersection.build(
types: [
AST::Builtin::Object.instance_type,
*module_entry.self_types.map {|module_self|
type = case
when module_self.name.interface?
RBS::Types::Interface.new(
name: module_self.name,
args: module_self.args,
location: module_self.location
)
when module_self.name.class?
RBS::Types::ClassInstance.new(
name: module_self.name,
args: module_self.args,
location: module_self.location
)
else
raise
end
checker.factory.type(type)
},
module_context.instance_type
].compact
module_entry = checker.factory.definition_builder.env.normalized_module_entry(implement_module_name.name)
if module_entry
module_context = module_context.update(
instance_type: AST::Types::Intersection.build(
types: [
AST::Builtin::Object.instance_type,
*module_entry.self_types.map {|module_self|
type = case
when module_self.name.interface?
RBS::Types::Interface.new(
name: module_self.name,
args: module_self.args,
location: module_self.location
)
when module_self.name.class?
RBS::Types::ClassInstance.new(
name: module_self.name,
args: module_self.args,
location: module_self.location
)
else
raise
end
checker.factory.type(type)
},
module_context.instance_type
].compact
)
)
)
elsif checker.factory.definition_builder.env.normalized_class_entry(implement_module_name.name)
typing.add_error(
Diagnostic::Ruby::ClassModuleMismatch.new(node: node, name: new_module_name)
)
end
end

if annots.instance_type
Expand Down Expand Up @@ -472,6 +477,13 @@ def for_class(node, new_class_name, super_class_name)
if super_class_name && implement_module_name.name == absolute_name(super_class_name)
module_context = module_context.update(instance_definition: nil, module_definition: nil)
end

if !checker.factory.definition_builder.env.normalized_class_entry(implement_module_name.name) &&
checker.factory.definition_builder.env.normalized_module_entry(implement_module_name.name)
typing.add_error(
Diagnostic::Ruby::ClassModuleMismatch.new(node: node, name: new_class_name)
)
end
else
module_context = module_context.update(
instance_type: AST::Builtin::Object.instance_type,
Expand Down
8 changes: 8 additions & 0 deletions sig/steep/diagnostic/ruby.rbs
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,14 @@ module Steep
def header_line: () -> ::String
end

class ClassModuleMismatch < Base
attr_reader name: RBS::TypeName

def initialize: (node: Parser::AST::Node, name: RBS::TypeName) -> void

def header_line: () -> ::String
end

class MethodArityMismatch < Base
attr_reader method_type: untyped

Expand Down
48 changes: 48 additions & 0 deletions test/type_check_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2579,4 +2579,52 @@ def test_masgn_splat
YAML
)
end

def test_class_module_mismatch
run_type_check_test(
signatures: {
"a.rbs" => <<~RBS
class Foo
end
module Bar
end
RBS
},
code: {
"a.rb" => <<~RUBY
module Foo
end
class Bar
end
RUBY
},
expectations: <<~YAML
---
- file: a.rb
diagnostics:
- range:
start:
line: 1
character: 7
end:
line: 1
character: 10
severity: ERROR
message: \"::Foo is declared as a class in RBS\"
code: Ruby::ClassModuleMismatch
- range:
start:
line: 4
character: 6
end:
line: 4
character: 9
severity: ERROR
message: \"::Bar is declared as a module in RBS\"
code: Ruby::ClassModuleMismatch
YAML
)
end
end
55 changes: 54 additions & 1 deletion test/type_construction_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1358,7 +1358,7 @@ def bar
with_standard_construction(checker, source) do |construction, typing|
construction.synthesize(source.node)

assert_typing_error(typing, size: 2) do |errors|
assert_typing_error(typing, size: 3) do |errors|
assert_any!(errors) do |error|
assert_instance_of Diagnostic::Ruby::IncompatibleAssignment, error
assert_equal parse_type("::String"), error.rhs_type
Expand All @@ -1370,6 +1370,11 @@ def bar
assert_equal parse_type("::String"), error.actual
assert_equal parse_type("::A::String"), error.expected
end

assert_any!(errors) do |error|
assert_instance_of Diagnostic::Ruby::ClassModuleMismatch, error
assert_equal "::A", error.name.to_s
end
end
end
end
Expand Down Expand Up @@ -5068,6 +5073,54 @@ module Module1
end
end

def test_module_type_mismatch
with_checker <<-EOF do |checker|
class SampleModule
end
EOF
source = parse_ruby(<<-EOF)
module SampleModule
end
EOF

with_standard_construction(checker, source) do |construction, typing|
construction.synthesize(source.node)

assert_typing_error(typing, size: 1) do |errors|
assert_any!(errors) do |error|
assert_instance_of Diagnostic::Ruby::ClassModuleMismatch, error
assert_equal '::SampleModule', error.name.to_s
end
end
end
end
end

def test_class_type_mismatch
with_checker <<-EOF do |checker|
module SampleClass
end
class SampleModule
end
EOF
source = parse_ruby(<<-EOF)
class SampleClass
end
EOF

with_standard_construction(checker, source) do |construction, typing|
construction.synthesize(source.node)

assert_typing_error(typing, size: 1) do |errors|
assert_any!(errors) do |error|
assert_instance_of Diagnostic::Ruby::ClassModuleMismatch, error
assert_equal '::SampleClass', error.name.to_s
end
end
end
end
end

def test_module_no_rbs
with_checker do |checker|
source = parse_ruby(<<-EOF)
Expand Down

0 comments on commit d7cb1f7

Please sign in to comment.