Skip to content

Commit 495a0e5

Browse files
committed
[GR-18163] Change order of module creation steps and assign a module's full name before calling the Module#const_added callback
PullRequest: truffleruby/4375
2 parents 9b191b0 + 791e5d3 commit 495a0e5

File tree

12 files changed

+132
-44
lines changed

12 files changed

+132
-44
lines changed

CHANGELOG.md

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ New features:
44

55
Bug fixes:
66

7+
* Fix `Module#name` called inside the `Module#const_added` callback when the module is defined in the top-level scope (#3683, @andrykonchin).
8+
* Fix duplicated calls of a `Module#const_added` callback when a module with nested modules is assigned to a constant (@andrykonchin).
9+
710
Compatibility:
811

912
* Fix `Module#include` so a module included into a reopened nested module is added into an ancestors chain (#3570, @andrykonchin).

spec/ruby/core/module/const_added_spec.rb

+43
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require_relative '../../spec_helper'
22
require_relative 'fixtures/classes'
3+
require_relative 'fixtures/const_added'
34

45
describe "Module#const_added" do
56
ruby_version_is "3.2" do
@@ -63,6 +64,27 @@ module SubModule
6364
ScratchPad.recorded.should == [:SubModule]
6465
end
6566

67+
it "is called when a new module is defined under a named module (assigned to a constant)" do
68+
ScratchPad.record []
69+
70+
ModuleSpecs::ConstAddedSpecs::NamedModule = Module.new do
71+
def self.const_added(name)
72+
ScratchPad << name
73+
end
74+
75+
module self::A
76+
def self.const_added(name)
77+
ScratchPad << name
78+
end
79+
80+
module self::B
81+
end
82+
end
83+
end
84+
85+
ScratchPad.recorded.should == [:A, :B]
86+
end
87+
6688
it "is called when a new class is defined under self" do
6789
ScratchPad.record []
6890

@@ -83,6 +105,27 @@ class SubClass
83105
ScratchPad.recorded.should == [:SubClass]
84106
end
85107

108+
it "is called when a new class is defined under a named module (assigned to a constant)" do
109+
ScratchPad.record []
110+
111+
ModuleSpecs::ConstAddedSpecs::NamedModuleB = Module.new do
112+
def self.const_added(name)
113+
ScratchPad << name
114+
end
115+
116+
class self::A
117+
def self.const_added(name)
118+
ScratchPad << name
119+
end
120+
121+
class self::B
122+
end
123+
end
124+
end
125+
126+
ScratchPad.recorded.should == [:A, :B]
127+
end
128+
86129
it "is called when an autoload is defined" do
87130
ScratchPad.record []
88131

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
module ModuleSpecs
2+
module ConstAddedSpecs
3+
end
4+
end

spec/ruby/core/module/fixtures/name.rb

+3
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,7 @@ def name
77
.name
88
end
99
end
10+
11+
module NameSpecs
12+
end
1013
end

spec/ruby/core/module/name_spec.rb

+41
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,47 @@ module ModuleSpecs::Anonymous
140140
valid_names.should include(m::N.name) # You get one of the two, but you don't know which one.
141141
end
142142

143+
ruby_version_is "3.2" do
144+
it "is set in #const_added callback when a module defined in the top-level scope" do
145+
ruby_exe(<<~RUBY, args: "2>&1").chomp.should == "TEST1\nTEST2"
146+
class Module
147+
def const_added(name)
148+
puts const_get(name).name
149+
end
150+
end
151+
152+
# module with name
153+
module TEST1
154+
end
155+
156+
# anonymous module
157+
TEST2 = Module.new
158+
RUBY
159+
end
160+
161+
it "is set in #const_added callback for a nested module when an outer module defined in the top-level scope" do
162+
ScratchPad.record []
163+
164+
ModuleSpecs::NameSpecs::NamedModule = Module.new do
165+
def self.const_added(name)
166+
ScratchPad << const_get(name).name
167+
end
168+
169+
module self::A
170+
def self.const_added(name)
171+
ScratchPad << const_get(name).name
172+
end
173+
174+
module self::B
175+
end
176+
end
177+
end
178+
179+
ScratchPad.recorded.should.one?(/#<Module.+>::A$/)
180+
ScratchPad.recorded.should.one?(/#<Module.+>::A::B$/)
181+
end
182+
end
183+
143184
it "returns a frozen String" do
144185
ModuleSpecs.name.should.frozen?
145186
end

spec/tags/core/module/name_tags.txt

+1
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
fails:Module#name is not nil when assigned to a constant in an anonymous module
2+
slow:Module#name is set in #const_added callback when a module defined in the top-level scope

src/main/java/org/truffleruby/core/CoreLibrary.java

+5-5
Original file line numberDiff line numberDiff line change
@@ -299,11 +299,11 @@ public CoreLibrary(RubyContext context, RubyLanguage language) {
299299
objectClass = (RubyClass) moduleClass.superclass;
300300
basicObjectClass = (RubyClass) objectClass.superclass;
301301

302-
// Set constants in Object and lexical parents
303-
classClass.fields.getAdoptedByLexicalParent(context, objectClass, "Class", node);
304-
basicObjectClass.fields.getAdoptedByLexicalParent(context, objectClass, "BasicObject", node);
305-
objectClass.fields.getAdoptedByLexicalParent(context, objectClass, "Object", node);
306-
moduleClass.fields.getAdoptedByLexicalParent(context, objectClass, "Module", node);
302+
// Set constants in Object
303+
objectClass.fields.setConstant(context, node, "Class", classClass);
304+
objectClass.fields.setConstant(context, node, "BasicObject", basicObjectClass);
305+
objectClass.fields.setConstant(context, node, "Object", objectClass);
306+
objectClass.fields.setConstant(context, node, "Module", moduleClass);
307307

308308
// Create Exception classes
309309

src/main/java/org/truffleruby/core/klass/ClassNodes.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ private static RubyClass createRubyClass(RubyContext context,
119119
superclass);
120120

121121
if (lexicalParent != null) {
122-
rubyClass.fields.getAdoptedByLexicalParent(context, lexicalParent, name, currentNode);
122+
lexicalParent.fields.setConstant(context, currentNode, name, rubyClass);
123123
}
124124

125125
return rubyClass;

src/main/java/org/truffleruby/core/module/ModuleFields.java

+28-37
Original file line numberDiff line numberDiff line change
@@ -165,53 +165,34 @@ public void afterConstructed() {
165165
getName();
166166
}
167167

168-
public RubyConstant getAdoptedByLexicalParent(
168+
private void nameModule(
169169
RubyContext context,
170170
RubyModule lexicalParent,
171-
String name,
172-
Node currentNode) {
171+
String name) {
173172
assert name != null;
174173

175-
RubyConstant previous = lexicalParent.fields.setConstantInternal(
176-
context,
177-
currentNode,
178-
name,
179-
rubyModule,
180-
false);
181-
182174
if (!hasFullName()) {
183-
// Tricky, we need to compare with the Object class, but we only have a Class at hand.
184-
final RubyClass classClass = getLogicalClass().getLogicalClass();
185-
final RubyClass objectClass = (RubyClass) ((RubyClass) classClass.superclass).superclass;
186-
187-
if (lexicalParent == objectClass) {
175+
if (lexicalParent == getObjectClass()) {
188176
this.setFullName(name);
189-
updateAnonymousChildrenModules(context);
177+
nameChildrenModules(context);
190178
} else if (lexicalParent.fields.hasFullName()) {
191179
this.setFullName(lexicalParent.fields.getName() + "::" + name);
192-
updateAnonymousChildrenModules(context);
180+
nameChildrenModules(context);
193181
} else {
194182
// Our lexicalParent is also an anonymous module
195183
// and will name us when it gets named via updateAnonymousChildrenModules(),
196184
// or we'll compute an anonymous name on #getName() if needed
197185
}
198186
}
199-
return previous;
200187
}
201188

202-
public void updateAnonymousChildrenModules(RubyContext context) {
189+
private void nameChildrenModules(RubyContext context) {
203190
for (Map.Entry<String, ConstantEntry> entry : constants.entrySet()) {
204191
ConstantEntry constantEntry = entry.getValue();
205192
RubyConstant constant = constantEntry.getConstant();
206-
if (constant != null && constant.hasValue() && constant.getValue() instanceof RubyModule) {
207-
RubyModule module = (RubyModule) constant.getValue();
208-
if (!module.fields.hasFullName()) {
209-
module.fields.getAdoptedByLexicalParent(
210-
context,
211-
rubyModule,
212-
entry.getKey(),
213-
null);
214-
}
193+
194+
if (constant != null && constant.hasValue() && constant.getValue() instanceof RubyModule childModule) {
195+
childModule.fields.nameModule(context, rubyModule, entry.getKey());
215196
}
216197
}
217198
}
@@ -429,15 +410,13 @@ private List<RubyModule> getPrependedModulesAndSelf() {
429410
/** Set the value of a constant, possibly redefining it. */
430411
@TruffleBoundary
431412
public RubyConstant setConstant(RubyContext context, Node currentNode, String name, Object value) {
432-
if (value instanceof RubyModule) {
433-
return ((RubyModule) value).fields.getAdoptedByLexicalParent(
434-
context,
435-
rubyModule,
436-
name,
437-
currentNode);
438-
} else {
439-
return setConstantInternal(context, currentNode, name, value, false);
413+
// A module fully qualified name should replace a temporary one before assigning a constant,
414+
// and before calling in the #const_added callback.
415+
if (value instanceof RubyModule childModule) {
416+
childModule.fields.nameModule(context, rubyModule, name);
440417
}
418+
419+
return setConstantInternal(context, currentNode, name, value, false);
441420
}
442421

443422
@TruffleBoundary
@@ -792,7 +771,11 @@ public Object getRubyStringName() {
792771
@TruffleBoundary
793772
private String createAnonymousName() {
794773
if (givenBaseName != null) {
795-
return lexicalParent.fields.getName() + "::" + givenBaseName;
774+
if (lexicalParent == getObjectClass()) {
775+
return givenBaseName;
776+
} else {
777+
return lexicalParent.fields.getName() + "::" + givenBaseName;
778+
}
796779
} else if (getLogicalClass() == rubyModule) { // For the case of class Class during initialization
797780
return "#<cyclic>";
798781
} else if (RubyGuards.isSingletonClass(rubyModule)) {
@@ -1181,4 +1164,12 @@ private void refinedMethod(RubyContext context, String methodName, Node currentN
11811164
}
11821165
}
11831166

1167+
private RubyClass getObjectClass() {
1168+
// Tricky, we need to compare with the Object class, but we only have a Module at hand.
1169+
final RubyClass classClass = getLogicalClass().getLogicalClass();
1170+
final RubyClass objectClass = (RubyClass) ((RubyClass) classClass.superclass).superclass;
1171+
assert objectClass.fields.givenBaseName == "Object";
1172+
return objectClass;
1173+
}
1174+
11841175
}

src/main/java/org/truffleruby/core/module/ModuleNodes.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -158,7 +158,7 @@ public static RubyModule createModule(RubyContext context, SourceSection sourceS
158158
module.fields.afterConstructed();
159159

160160
if (lexicalParent != null) {
161-
module.fields.getAdoptedByLexicalParent(context, lexicalParent, name, currentNode);
161+
lexicalParent.fields.setConstant(context, currentNode, name, module);
162162
}
163163
return module;
164164
}

test/mri/excludes/TestSetTraceFunc.rb

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
exclude :test_raise, "<[\"c-return\", 1, :set_trace_func, Kernel]> expected but was <nil>."
2727
exclude :test_rb_rescue, "ArgumentError: unknown event: a_call"
2828
exclude :test_recursive, "| <internal:core> core/tracepoint.rb:24:in `block in initialize': unknown event: c_call (ArgumentError)"
29+
exclude :test_remove_in_trace, "transient: [ruby-dev:42350]. <#<TestSetTraceFunc:...> expected but was <false>"
2930
exclude :test_rescue_and_ensure_should_not_cause_b_return, "ArgumentError: unknown event: a_call"
3031
exclude :test_return, "<[\"c-return\", 1, :set_trace_func, Kernel]> expected but was <nil>."
3132
exclude :test_return2, "<[\"c-return\", 1, :set_trace_func, Kernel]> expected but was <nil>."

test/mri/excludes/TestThread.rb

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
exclude :test_abort_on_exception, "| Exception: `RuntimeError' at -:3:in `block in <main>' - "
2+
exclude :test_handle_interrupt, "transient: <[:on_blocking, :c1]> expected but was <[:on_blocking, :c2]>."
23
exclude :test_handle_interrupt_and_io, "needs investigation"
34
exclude :test_handle_interrupt_and_p, "transient"
45
exclude :test_handle_interrupt_blocking, "<:ok> expected but was <nil>."

0 commit comments

Comments
 (0)