From b1821cc0665e2dc353f2a5d7b5e58e533338176a Mon Sep 17 00:00:00 2001 From: SamW Date: Tue, 9 Jun 2026 13:55:42 -0700 Subject: [PATCH] Add the %a{warning: ...} annotation in --- lib/steep/annotations_helper.rb | 66 ++++++--- lib/steep/diagnostic/lsp_formatter.rb | 2 + lib/steep/diagnostic/ruby.rb | 33 +++++ lib/steep/diagnostic/signature.rb | 20 +++ lib/steep/signature/validator.rb | 14 ++ lib/steep/type_construction.rb | 121 +++++++++++++--- manual/ruby-diagnostics.md | 51 +++++++ sig/steep/annotations_helper.rbs | 14 ++ sig/steep/diagnostic/ruby.rbs | 50 +++++++ sig/steep/diagnostic/signature.rbs | 10 ++ sig/steep/signature/validator.rbs | 4 + sig/steep/type_construction.rbs | 14 ++ test/type_check_test.rb | 201 ++++++++++++++++++++++++++ test/validation_test.rb | 34 +++++ 14 files changed, 593 insertions(+), 41 deletions(-) diff --git a/lib/steep/annotations_helper.rb b/lib/steep/annotations_helper.rb index 7d7853895..6a7be2816 100644 --- a/lib/steep/annotations_helper.rb +++ b/lib/steep/annotations_helper.rb @@ -15,39 +15,59 @@ def deprecated_annotation?(annotations) nil end - def deprecated_type_name?(type_name, env) - annotations = + def warning_annotation?(annotations) + annotations.reverse_each do |annotation| + if match = annotation.string.match(/\Awarning(:\s*(?.+))?\z/) + return [annotation, match[:message]] + end + if match = annotation.string.match(/\Asteep:warning(:\s*(?.+))?\z/) + return [annotation, match[:message]] + end + end + + nil + end + + def type_name_annotations(type_name, env) + case + when type_name.class? case - when type_name.class? - case - when decl = env.class_decls.fetch(type_name, nil) - decl.each_decl.flat_map do |decl| - if decl.is_a?(RBS::AST::Declarations::Base) - decl.annotations - else - [] - end - end - when decl = env.class_alias_decls.fetch(type_name, nil) - if decl.decl.is_a?(RBS::AST::Declarations::Base) - decl.decl.annotations + when decl = env.class_decls.fetch(type_name, nil) + decl.each_decl.flat_map do |decl| + if decl.is_a?(RBS::AST::Declarations::Base) + decl.annotations else - [] #: Array[RBS::AST::Annotation] + [] end end - when type_name.interface? - if decl = env.interface_decls.fetch(type_name, nil) - decl.decl.annotations - end - when type_name.alias? - if decl = env.type_alias_decls.fetch(type_name, nil) + when decl = env.class_alias_decls.fetch(type_name, nil) + if decl.decl.is_a?(RBS::AST::Declarations::Base) decl.decl.annotations + else + [] #: Array[RBS::AST::Annotation] end end + when type_name.interface? + if decl = env.interface_decls.fetch(type_name, nil) + decl.decl.annotations + end + when type_name.alias? + if decl = env.type_alias_decls.fetch(type_name, nil) + decl.decl.annotations + end + end + end - if annotations + def deprecated_type_name?(type_name, env) + if annotations = type_name_annotations(type_name, env) deprecated_annotation?(annotations) end end + + def warning_type_name?(type_name, env) + if annotations = type_name_annotations(type_name, env) + warning_annotation?(annotations) + end + end end end diff --git a/lib/steep/diagnostic/lsp_formatter.rb b/lib/steep/diagnostic/lsp_formatter.rb index c56b42d22..65aecf16e 100644 --- a/lib/steep/diagnostic/lsp_formatter.rb +++ b/lib/steep/diagnostic/lsp_formatter.rb @@ -57,6 +57,8 @@ def format(diagnostic) when Signature::DeprecatedTypeName tags << LSP::Constant::DiagnosticTag::DEPRECATED severity = LSP::Constant::DiagnosticSeverity::WARNING + when Signature::WarningTypeName + severity = LSP::Constant::DiagnosticSeverity::WARNING end json = { diff --git a/lib/steep/diagnostic/ruby.rb b/lib/steep/diagnostic/ruby.rb index 9f3f005c1..bab5e991f 100644 --- a/lib/steep/diagnostic/ruby.rb +++ b/lib/steep/diagnostic/ruby.rb @@ -1080,6 +1080,36 @@ def header_line end end + class WarningReference < Base + attr_reader :message + + def initialize(node:, location:, message:) + super(node: node, location: location) + @message = message + end + + def header_line + header = + case node&.type + when :send, :csend, :block, :numblock + "The method has a warning" + when :const, :casgn + "The constant has a warning" + when :gvar, :gvasgn + "The global variable has a warning" + else + raise "Unexpected node: #{node}" + end + + if message + header = +header + header.concat(": ", message) + end + + header + end + end + ALL = ObjectSpace.each_object(Class).with_object([]) do |klass, array| if klass < Base array << klass @@ -1101,6 +1131,7 @@ def self.default BlockTypeMismatch => :warning, BreakTypeMismatch => :hint, DeprecatedReference => :warning, + WarningReference => :warning, DifferentMethodParameterKind => :hint, FallbackAny => :hint, FalseAssertion => :hint, @@ -1166,6 +1197,7 @@ def self.strict BlockTypeMismatch => :error, BreakTypeMismatch => :error, DeprecatedReference => :warning, + WarningReference => :warning, DifferentMethodParameterKind => :error, FallbackAny => :warning, FalseAssertion => :error, @@ -1230,6 +1262,7 @@ def self.lenient BlockTypeMismatch => :information, BreakTypeMismatch => :hint, DeprecatedReference => :warning, + WarningReference => :warning, DifferentMethodParameterKind => nil, FallbackAny => nil, FalseAssertion => nil, diff --git a/lib/steep/diagnostic/signature.rb b/lib/steep/diagnostic/signature.rb index 67e7d0776..a5983bb54 100644 --- a/lib/steep/diagnostic/signature.rb +++ b/lib/steep/diagnostic/signature.rb @@ -515,6 +515,26 @@ def header_line end end + class WarningTypeName < Base + attr_reader :type_name + attr_reader :message + + def initialize(type_name, message, location:) + super(location: location) + @type_name = type_name + @message = message + end + + def header_line + buffer = "Type `#{type_name}` has a warning" + if message + buffer = +buffer + buffer << ": " << message + end + buffer + end + end + class InlineDiagnostic < Base attr_reader :diagnostic diff --git a/lib/steep/signature/validator.rb b/lib/steep/signature/validator.rb index 6f1a69323..34647743c 100644 --- a/lib/steep/signature/validator.rb +++ b/lib/steep/signature/validator.rb @@ -166,6 +166,7 @@ def validate_type_0(type) if type_name && location validate_type_name_deprecation(type_name, location) + validate_type_name_warning(type_name, location) end type.each_type do |child| @@ -179,6 +180,12 @@ def validate_type_name_deprecation(type_name, location) end end + def validate_type_name_warning(type_name, location) + if (_, message = AnnotationsHelper.warning_type_name?(type_name, env)) + @errors << Diagnostic::Signature::WarningTypeName.new(type_name, message, location: location) + end + end + def ancestor_to_type(ancestor) case ancestor when RBS::Definition::Ancestor::Instance @@ -328,6 +335,11 @@ def validate_one_class_decl(name, entry) validate_type_name_deprecation(name, location[:name]) end end + unless AnnotationsHelper.warning_annotation?(decl.annotations) + if location = decl.location + validate_type_name_warning(name, location[:name]) + end + end end end @@ -401,6 +413,7 @@ def validate_one_class_decl(name, entry) end if location validate_type_name_deprecation(ancestor.name, location) + validate_type_name_warning(ancestor.name, location) end end end @@ -695,6 +708,7 @@ def validate_one_class_alias(name, entry) validator.validate_class_alias(entry: entry) if location = entry.decl.location validate_type_name_deprecation(entry.decl.old_name, location[:old_name]) + validate_type_name_warning(entry.decl.old_name, location[:old_name]) end end end diff --git a/lib/steep/type_construction.rb b/lib/steep/type_construction.rb index 5d6dee06c..dc5c4d473 100644 --- a/lib/steep/type_construction.rb +++ b/lib/steep/type_construction.rb @@ -1511,6 +1511,7 @@ def synthesize(node, hint: nil, condition: false) if class_name check_deprecation_constant(class_name, name_node, name_node.location.expression) + check_warning_constant(class_name, name_node, name_node.location.expression) end else _, constr = synthesize(name_node) @@ -1570,6 +1571,7 @@ def synthesize(node, hint: nil, condition: false) if module_name check_deprecation_constant(module_name, name_node, name_node.location.expression) + check_warning_constant(module_name, name_node, name_node.location.expression) end else _, constr = synthesize(name_node) @@ -1642,6 +1644,7 @@ def synthesize(node, hint: nil, condition: false) if name typing.source_index.add_reference(constant: name, ref: node) constr.check_deprecation_constant(name, node, node.location.expression) + constr.check_warning_constant(name, node, node.location.expression) end Pair.new(type: type, constr: constr) @@ -1662,6 +1665,7 @@ def synthesize(node, hint: nil, condition: false) typing.source_index.add_definition(constant: constant_name, definition: node) location = node.location #: Parser::Source::Map & Parser::AST::_Variable constr.check_deprecation_constant(constant_name, node, location.name) + constr.check_warning_constant(constant_name, node, location.name) end value_type, constr = constr.synthesize(node.children.last, hint: constant_type) @@ -2438,6 +2442,7 @@ def synthesize(node, hint: nil, condition: false) location = node.location #: Parser::Source::Map & Parser::AST::_Variable constr.check_deprecation_global(name, node, location.name) + constr.check_warning_global(name, node, location.name) type, constr = constr.gvasgn(node, rhs_type) @@ -2449,6 +2454,7 @@ def synthesize(node, hint: nil, condition: false) name = node.children.first check_deprecation_global(name, node, node.location.expression) + check_warning_global(name, node, node.location.expression) if type = context.type_env[name] add_typing(node, type: type) @@ -3277,6 +3283,23 @@ def deprecated_send?(call) nil end + def warning_send?(call) + case call.node.type + when :send, :csend, :block, :numblock + # supported + else + return + end + + call.method_decls.each do |decl| + if pair = AnnotationsHelper.warning_annotation?(decl.method_def.each_annotation.to_a) + return pair + end + end + + nil + end + def type_send_interface(node, interface:, receiver:, receiver_type:, method_name:, arguments:, block_params:, block_body:, tapp:, hint:) method = interface.methods[method_name] @@ -3344,6 +3367,33 @@ def type_send_interface(node, interface:, receiver:, receiver_type:, method_name ) ) end + + if (_, message = warning_send?(call)) + send_node = + case node.type + when :block, :numblock + node.children[0] + else + node + end + + # `send_node` is usually a `:send`/`:csend` node with a selector, but a + # block can also wrap a `super`/`zsuper` node which has no selector. + location = + if (_, _, _, loc = deconstruct_send_node(send_node)) + loc.selector + else + send_node.location.expression + end + + constr.typing.add_error( + Diagnostic::Ruby::WarningReference.new( + node: node, + location: location, + message: message + ) + ) + end end if node.type == :csend || ((node.type == :block || node.type == :numblock) && node.children[0].type == :csend) @@ -5338,9 +5388,15 @@ def instance_type(type) end end - def check_deprecation_global(name, node, location) + private def global_annotations(name) if global_entry = checker.factory.env.global_decls[name] - if (_, message = AnnotationsHelper.deprecated_annotation?(global_entry.decl.annotations)) + global_entry.decl.annotations + end + end + + def check_deprecation_global(name, node, location) + if annotations = global_annotations(name) + if (_, message = AnnotationsHelper.deprecated_annotation?(annotations)) typing.add_error( Diagnostic::Ruby::DeprecatedReference.new( node: node, @@ -5352,28 +5408,43 @@ def check_deprecation_global(name, node, location) end end - def check_deprecation_constant(name, node, location) + def check_warning_global(name, node, location) + if annotations = global_annotations(name) + if (_, message = AnnotationsHelper.warning_annotation?(annotations)) + typing.add_error( + Diagnostic::Ruby::WarningReference.new( + node: node, + location: location, + message: message + ) + ) + end + end + end + + private def constant_annotations(name) entry = checker.builder.factory.env.constant_entry(name) - annotations = - case entry - when RBS::Environment::ModuleEntry, RBS::Environment::ClassEntry - entry.each_decl.flat_map do |decl| - if decl.is_a?(RBS::AST::Declarations::Base) - decl.annotations - else - [] - end - end - when RBS::Environment::ConstantEntry, RBS::Environment::ClassAliasEntry, RBS::Environment::ModuleAliasEntry - if entry.decl.is_a?(RBS::AST::Declarations::Base) - entry.decl.annotations + case entry + when RBS::Environment::ModuleEntry, RBS::Environment::ClassEntry + entry.each_decl.flat_map do |decl| + if decl.is_a?(RBS::AST::Declarations::Base) + decl.annotations else - [] #: Array[RBS::AST::Annotation] + [] end end + when RBS::Environment::ConstantEntry, RBS::Environment::ClassAliasEntry, RBS::Environment::ModuleAliasEntry + if entry.decl.is_a?(RBS::AST::Declarations::Base) + entry.decl.annotations + else + [] #: Array[RBS::AST::Annotation] + end + end + end - if annotations + def check_deprecation_constant(name, node, location) + if annotations = constant_annotations(name) if (_, message = AnnotationsHelper.deprecated_annotation?(annotations)) typing.add_error( Diagnostic::Ruby::DeprecatedReference.new( @@ -5385,5 +5456,19 @@ def check_deprecation_constant(name, node, location) end end end + + def check_warning_constant(name, node, location) + if annotations = constant_annotations(name) + if (_, message = AnnotationsHelper.warning_annotation?(annotations)) + typing.add_error( + Diagnostic::Ruby::WarningReference.new( + node: node, + location: location, + message: message + ) + ) + end + end + end end end diff --git a/manual/ruby-diagnostics.md b/manual/ruby-diagnostics.md index 3b394ee4b..260c8a5e9 100644 --- a/manual/ruby-diagnostics.md +++ b/manual/ruby-diagnostics.md @@ -1990,3 +1990,54 @@ The syntax is not currently supported by Steep. | - | - | - | - | - | | error | information | hint | hint | - | + +## Ruby::WarningReference + +Method call, constant reference, or global variable reference resolves to a +declaration (or method overload) that carries a `%a{warning: ...}` annotation. + +This works like `%a{deprecated}` but is intended for arbitrary, author-defined +caveats rather than deprecation specifically. Unlike deprecation, it can also be +attached to a single method overload to warn only when that overload is selected. + +### RBS + +```rbs +%a{warning: experimental} class Foo +end + +class Bar + def baz: () -> void + | %a{warning: don't pass an Integer} (Integer) -> void +end +``` + +### Ruby code + +```ruby +Foo + +Bar.new.baz(1) +``` + +### Diagnostic + +``` +lib/warning.rb:1:0: [warning] The constant has a warning: experimental +│ Diagnostic ID: Ruby::WarningReference +│ +└ Foo + ~~~ + +lib/warning.rb:3:8: [warning] The method has a warning: don't pass an Integer +│ Diagnostic ID: Ruby::WarningReference +│ +└ Bar.new.baz(1) + ~~~ +``` + +### Severity + +| all_error | strict | default | lenient | silent | +| - | - | - | - | - | +| error | warning | warning | warning | - | diff --git a/sig/steep/annotations_helper.rbs b/sig/steep/annotations_helper.rbs index d10d4446b..a1ebb7f72 100644 --- a/sig/steep/annotations_helper.rbs +++ b/sig/steep/annotations_helper.rbs @@ -10,8 +10,22 @@ module Steep # def self?.deprecated_annotation?: (Array[Annotation]) -> [Annotation, String?]? + # Returns true if the array of annotations declared a *warning* + # + # Returns the pair of the annotation and given message. + # + def self?.warning_annotation?: (Array[Annotation]) -> [Annotation, String?]? + + # Returns the annotations attached to the declarations of the given type name. + # + def self?.type_name_annotations: (RBS::TypeName, RBS::Environment) -> Array[Annotation]? + # Returns truthy if the type name is declared *deprecated* # def self?.deprecated_type_name?: (RBS::TypeName, RBS::Environment) -> [Annotation, String?]? + + # Returns truthy if the type name is declared with a *warning* + # + def self?.warning_type_name?: (RBS::TypeName, RBS::Environment) -> [Annotation, String?]? end end diff --git a/sig/steep/diagnostic/ruby.rbs b/sig/steep/diagnostic/ruby.rbs index c907c93fc..36890a26b 100644 --- a/sig/steep/diagnostic/ruby.rbs +++ b/sig/steep/diagnostic/ruby.rbs @@ -2014,6 +2014,56 @@ module Steep def header_line: () -> void end + # Method call, constant reference, or global variable reference resolves to a + # declaration (or method overload) that carries a `%a{warning: ...}` annotation. + # + # This works like `%a{deprecated}` but is intended for arbitrary, author-defined + # caveats rather than deprecation specifically. Unlike deprecation, it can also be + # attached to a single method overload to warn only when that overload is selected. + # + # ### RBS + # + # ```rbs + # %a{warning: experimental} class Foo end + # + # class Bar + # def baz: () -> void + # | %a{warning: don't pass an Integer} (Integer) -> void + # end + # ``` + # + # ### Ruby code + # + # ```ruby + # Foo + # + # Bar.new.baz(1) + # ``` + # + # ### Diagnostic + # + # ``` + # lib/warning.rb:1:0: [warning] The constant has a warning: experimental + # │ Diagnostic ID: Ruby::WarningReference + # │ + # └ Foo + # ~~~ + # + # lib/warning.rb:3:8: [warning] The method has a warning: don't pass an Integer + # │ Diagnostic ID: Ruby::WarningReference + # │ + # └ Bar.new.baz(1) + # ~~~ + # ``` + # + class WarningReference < Base + attr_reader message: String? + + def initialize: (node: Node, location: location, message: String?) -> void + + def header_line: () -> void + end + ALL: Array[singleton(Base)] type template = Hash[singleton(Base), LSPFormatter::severity?] diff --git a/sig/steep/diagnostic/signature.rbs b/sig/steep/diagnostic/signature.rbs index 28597e8c3..c6b45afc3 100644 --- a/sig/steep/diagnostic/signature.rbs +++ b/sig/steep/diagnostic/signature.rbs @@ -310,6 +310,16 @@ module Steep def header_line: () -> String end + class WarningTypeName < Base + attr_reader type_name: RBS::TypeName + + attr_reader message: String? + + def initialize: (RBS::TypeName type_name, String? message, location: RBS::Location[untyped, untyped]?) -> void + + def header_line: () -> String + end + class InlineDiagnostic < Base attr_reader diagnostic: RBS::InlineParser::Diagnostic::t diff --git a/sig/steep/signature/validator.rbs b/sig/steep/signature/validator.rbs index 4f967b30d..db4ec1f8d 100644 --- a/sig/steep/signature/validator.rbs +++ b/sig/steep/signature/validator.rbs @@ -65,6 +65,10 @@ module Steep # private def validate_type_name_deprecation: (RBS::TypeName, RBS::Location[untyped, untyped]) -> void + # Validate if type name has a warning + # + private def validate_type_name_warning: (RBS::TypeName, RBS::Location[untyped, untyped]) -> void + # Validate a type and its descendants # def validate_type: (RBS::Types::t `type`) -> void diff --git a/sig/steep/type_construction.rbs b/sig/steep/type_construction.rbs index aa55121f7..4d3f99de0 100644 --- a/sig/steep/type_construction.rbs +++ b/sig/steep/type_construction.rbs @@ -536,16 +536,30 @@ module Steep def deprecated_send?: (TypeInference::MethodCall::Typed call) -> [RBS::AST::Annotation, String?]? + def warning_send?: (TypeInference::MethodCall::Typed call) -> [RBS::AST::Annotation, String?]? + def type_name: (AST::Types::t) -> RBS::TypeName? def singleton_type: (AST::Types::t) -> AST::Types::t? def instance_type: (AST::Types::t) -> AST::Types::t? + # Returns the annotations attached to the declaration of the given global variable + private def global_annotations: (Symbol name) -> Array[RBS::AST::Annotation]? + # Check if the global variable is deprecated and report diagnostic if so def check_deprecation_global: (Symbol name, Parser::AST::Node, Parser::Source::Range) -> void + # Check if the global variable has a warning and report diagnostic if so + def check_warning_global: (Symbol name, Parser::AST::Node, Parser::Source::Range) -> void + + # Returns the annotations attached to the declarations of the given constant + private def constant_annotations: (RBS::TypeName name) -> Array[RBS::AST::Annotation]? + # Check if the constant is deprecated and report diagnostic if so def check_deprecation_constant: (RBS::TypeName, Parser::AST::Node, Parser::Source::Range) -> void + + # Check if the constant has a warning and report diagnostic if so + def check_warning_constant: (RBS::TypeName, Parser::AST::Node, Parser::Source::Range) -> void end end diff --git a/test/type_check_test.rb b/test/type_check_test.rb index 5d70a1d28..e97c6f711 100644 --- a/test/type_check_test.rb +++ b/test/type_check_test.rb @@ -3359,6 +3359,207 @@ def foo: () -> void ) end + def test_warning_method_overload + run_type_check_test( + signatures: { + "a.rbs" => <<~RBS + class WithNonNil + def <=>: (untyped) -> Integer + end + + class WithNil + def <=>: (untyped) -> Integer? + end + + class Coll + def pick: () { (untyped, untyped) -> Integer } -> void + | %a{warning: nil comparison will always raise} () { (untyped, untyped) -> Integer? } -> void + end + RBS + }, + code: { + "a.rb" => <<~RUBY + Coll.new.pick { |a, b| WithNonNil.new <=> b } + Coll.new.pick { |a, b| WithNil.new <=> b } + RUBY + }, + expectations: <<~YAML + --- + - file: a.rb + diagnostics: + - range: + start: + line: 2 + character: 9 + end: + line: 2 + character: 13 + severity: ERROR + message: 'The method has a warning: nil comparison will always raise' + code: Ruby::WarningReference + YAML + ) + end + + def test_warning_method + run_type_check_test( + signatures: { + "a.rbs" => <<~RBS + class Foo + %a{steep:warning} def foo: () -> void + + %a{warning: Don't use bar} def bar: () -> void + end + RBS + }, + code: { + "a.rb" => <<~RUBY + Foo.new.foo() + + Foo.new.bar() + RUBY + }, + expectations: <<~YAML + --- + - file: a.rb + diagnostics: + - range: + start: + line: 1 + character: 8 + end: + line: 1 + character: 11 + severity: ERROR + message: The method has a warning + code: Ruby::WarningReference + - range: + start: + line: 3 + character: 8 + end: + line: 3 + character: 11 + severity: ERROR + message: 'The method has a warning: Don''t use bar' + code: Ruby::WarningReference + YAML + ) + end + + def test_warning_constant + run_type_check_test( + signatures: { + "a.rbs" => <<~RBS + %a{warning: heads up} FOO: Integer + RBS + }, + code: { + "a.rb" => <<~RUBY + FOO = 123 + FOO + RUBY + }, + expectations: <<~YAML + --- + - file: a.rb + diagnostics: + - range: + start: + line: 1 + character: 0 + end: + line: 1 + character: 3 + severity: ERROR + message: 'The constant has a warning: heads up' + code: Ruby::WarningReference + - range: + start: + line: 2 + character: 0 + end: + line: 2 + character: 3 + severity: ERROR + message: 'The constant has a warning: heads up' + code: Ruby::WarningReference + YAML + ) + end + + def test_warning_global + run_type_check_test( + signatures: { + "a.rbs" => <<~RBS + %a{warning: heads up} $FOO: Integer + RBS + }, + code: { + "a.rb" => <<~RUBY + $FOO = 123 + $FOO + RUBY + }, + expectations: <<~YAML + --- + - file: a.rb + diagnostics: + - range: + start: + line: 1 + character: 0 + end: + line: 1 + character: 4 + severity: ERROR + message: 'The global variable has a warning: heads up' + code: Ruby::WarningReference + - range: + start: + line: 2 + character: 0 + end: + line: 2 + character: 4 + severity: ERROR + message: 'The global variable has a warning: heads up' + code: Ruby::WarningReference + YAML + ) + end + + def test_warning_class_module + run_type_check_test( + signatures: { + "a.rbs" => <<~RBS + %a{warning: experimental} class Foo + end + RBS + }, + code: { + "a.rb" => <<~RUBY + Foo + RUBY + }, + expectations: <<~YAML + --- + - file: a.rb + diagnostics: + - range: + start: + line: 1 + character: 0 + end: + line: 1 + character: 3 + severity: ERROR + message: 'The constant has a warning: experimental' + code: Ruby::WarningReference + YAML + ) + end + def test_deprecated_class_module run_type_check_test( signatures: { diff --git a/test/validation_test.rb b/test/validation_test.rb index bcfe10dd6..6c836d4eb 100644 --- a/test/validation_test.rb +++ b/test/validation_test.rb @@ -1357,6 +1357,40 @@ class Bar < Foo end end + def test_validate__warning__class + with_checker <<~RBS do |checker| + %a{warning: experimental} class Foo end + + %a{warning: experimental} module M end + + %a{warning: experimental} interface _Foo end + + class Bar < Foo + include M + include _Foo + end + RBS + + Validator.new(checker: checker).tap do |validator| + validator.validate + assert_predicate validator, :has_error? + + assert_any!(validator.each_error) do |error| + assert_instance_of Diagnostic::Signature::WarningTypeName, error + assert_equal "Type `::Foo` has a warning: experimental", error.header_line + end + assert_any!(validator.each_error) do |error| + assert_instance_of Diagnostic::Signature::WarningTypeName, error + assert_equal "Type `::M` has a warning: experimental", error.header_line + end + assert_any!(validator.each_error) do |error| + assert_instance_of Diagnostic::Signature::WarningTypeName, error + assert_equal "Type `::_Foo` has a warning: experimental", error.header_line + end + end + end + end + def test_validate__deprecated__module with_checker <<~RBS do |checker| %a{deprecated} class Foo end