Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
106 changes: 106 additions & 0 deletions internal/vm/deprecation_warning_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright (c) the go-embedded-ruby/ruby authors
//
// SPDX-License-Identifier: BSD-3-Clause

package vm

import "testing"

// capture installs a Warning.warn that records the message (with the file name
// folded away, so the expectation is about the LINE and the text) and the
// category, which is how MRI delivers rb_category_warn.
const capture = `
$seen = []
module Warning
def self.warn(msg, category: nil)
$seen << [msg.sub(/\A.*?:(\d+): /, 'L\1: '), category]
end
end
`

// ObjectSpace._id2ref is deprecated in Ruby 4.0 — gc.c calls
// rb_category_warn(RB_WARN_CATEGORY_DEPRECATED, "ObjectSpace._id2ref is
// deprecated"). It still answers; it only says so.
//
// ⛔ THE GATE IS TWO CONDITIONS and only one is obvious. MRI's
// deprecation_warning_enabled() wants a NON-NIL $VERBOSE *and*
// Warning[:deprecated]. Measured on MRI 4.0.7: `$VERBOSE = nil` with the
// category on is silent; `$VERBOSE = false` with it on warns; `$VERBOSE = true`
// with the category OFF is silent. Every row below is one of those runs.
func TestId2refIsDeprecated(t *testing.T) {
for _, tc := range []struct{ name, src, want string }{
{"it warns when the category is on",
capture + `Warning[:deprecated] = true
ObjectSpace._id2ref(nil.object_id)
p $seen`,
`[["L9: warning: ObjectSpace._id2ref is deprecated\n", :deprecated]]`},

{"the category reaches a Warning.warn override",
capture + `Warning[:deprecated] = true
ObjectSpace._id2ref(nil.object_id)
p $seen[0][1]`,
`:deprecated`},

{"it is silent when the category is off",
capture + `Warning[:deprecated] = false
ObjectSpace._id2ref(nil.object_id)
p $seen`,
`[]`},

// ⛔ $VERBOSE = nil silences it even with the category on. Checking the
// category alone — which is what this repository's `iterator?` did
// before warnDeprecated existed — makes rbgo speak where MRI says
// nothing, and nil is how a program asks for exactly that silence.
{"a nil $VERBOSE silences it",
capture + `Warning[:deprecated] = true
$VERBOSE = nil
ObjectSpace._id2ref(nil.object_id)
p $seen`,
`[]`},

{"a false $VERBOSE does not silence it",
capture + `Warning[:deprecated] = true
$VERBOSE = false
ObjectSpace._id2ref(nil.object_id)
p $seen.size`,
`1`},

{"a true $VERBOSE alone is not enough",
capture + `$VERBOSE = true
ObjectSpace._id2ref(nil.object_id)
p $seen`,
`[]`},

// The deprecation does not replace the behaviour.
{"it still answers",
capture + `Warning[:deprecated] = true
s = "hi"
p ObjectSpace._id2ref(s.object_id).equal?(s)`,
`true`},

// ⛔ The arity check stays FIRST: calling it with no argument raises
// without warning, as MRI does. A warning emitted before the check
// would make a plain mistake look like a deprecation.
{"a missing argument raises without warning",
capture + `Warning[:deprecated] = true
begin; ObjectSpace._id2ref; rescue ArgumentError; end
p $seen`,
`[]`},

// The same gate, on the method warnDeprecated was extracted from.
{"iterator? follows the same gate",
capture + `Warning[:deprecated] = true
$VERBOSE = nil
iterator?
$VERBOSE = false
iterator?
p $seen.map { |m, c| [m[/L\d+/], c] }`,
`[["L12", :deprecated]]`},
} {
t.Run(tc.name, func(t *testing.T) {
if got := runSrc(t, tc.src); got != tc.want {
t.Errorf("%s\n got %s\nwant %s", tc.src, got, tc.want)
}
})
}
}
28 changes: 28 additions & 0 deletions internal/vm/globals.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,34 @@ func (vm *VM) warnDeprecatedGvar(msg string) {
vm.send(w, "warn", []object.Value{object.NewString("warning: " + msg + "\n")}, nil)
}

// warnDeprecated is error.c's rb_category_warn(RB_WARN_CATEGORY_DEPRECATED, …):
// the caller's file and line, then the message, carrying the :deprecated
// category, and emitted only when deprecation_warning_enabled() agrees.
//
// ⛔ THAT GATE IS TWO CONDITIONS, and only one of them is the obvious one.
// MRI requires a NON-NIL $VERBOSE *and* Warning[:deprecated]; measured on
// MRI 4.0.7, `$VERBOSE = nil` with the category on is silent, while
// `$VERBOSE = false` with the category on warns. Checking only the category —
// which is what `iterator?` did here until this function existed — makes rbgo
// speak where MRI says nothing, and `$VERBOSE = nil` is how a program asks for
// exactly that silence.
//
// The category is not decoration either: a `Warning.warn` override receives it
// as `category: :deprecated`, and a handler that filters on it saw nil from the
// old call site.
func (vm *VM) warnDeprecated(msg string) {
if !vm.warnEnabled() {
return
}
w := vm.consts["Warning"]
if w == nil || !vm.send(w, "[]", []object.Value{object.SymVal("deprecated")}, nil).Truthy() {
return
}
// warnUplevelPrefix already ends in "warning: " — it is the whole
// "<file>:<line>: warning: " head MRI prints, not just the location.
vm.warningWarn(object.NewString(vm.warnUplevelPrefix(0)+msg+"\n"), object.SymVal("deprecated"))
}

// gvarGet is variable.c's rb_gvar_get (variable.c:1052 at v4.0.9): the
// RUBY-LEVEL read of a global, which is gvar plus the warning MRI emits for a
// global nothing ever assigned. The getgvar instruction and the two AOT tiers
Expand Down
8 changes: 5 additions & 3 deletions internal/vm/kernel_introspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -218,9 +218,11 @@ func (vm *VM) registerKernelIntrospection() {
if len(args) != 0 {
raise("ArgumentError", "wrong number of arguments (given %d, expected 0)", len(args))
}
if vm.send(vm.consts["Warning"], "[]", []object.Value{object.SymVal("deprecated")}, nil).Truthy() {
vm.writeWarningStr(vm.warnUplevelPrefix(0) + "iterator? is deprecated; use block_given? instead\n")
}
// ⛔ This used to test the category alone and write the string itself,
// which warned where MRI is silent ($VERBOSE = nil) and handed a
// Warning.warn override `category: nil`. warnDeprecated is MRI's own
// gate; both divergences were measured against 4.0.7 before the change.
vm.warnDeprecated("iterator? is deprecated; use block_given? instead")
return object.Bool(vm.callerBlockGiven())
})
// Both are rb_define_global_function, i.e. the module-function split: a PRIVATE
Expand Down
6 changes: 6 additions & 0 deletions internal/vm/objectspace.go
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,12 @@ func (vm *VM) registerObjectSpace() {
if len(args) == 0 {
raise("ArgumentError", "wrong number of arguments (given 0, expected 1)")
}
// Deprecated in 4.0 (gc.c: rb_category_warn(RB_WARN_CATEGORY_DEPRECATED,
// "ObjectSpace._id2ref is deprecated")). It still answers; it only says
// so, and only when the :deprecated category is on — the argument check
// above stays FIRST, so `ObjectSpace._id2ref` with no argument raises
// without warning, as MRI does.
vm.warnDeprecated("ObjectSpace._id2ref is deprecated")
id := intArg(args[0])
if id&1 == 1 { // odd => fixnum id 2n+1
return object.IntValue(id >> 1)
Expand Down
Loading