Skip to content

Commit

Permalink
[GR-59172] Backport to 24.1: Change order of module creation steps an…
Browse files Browse the repository at this point in the history
…d assign a module's full name before calling the Module#const_added callback

PullRequest: truffleruby/4383
  • Loading branch information
andrykonchin authored and ansalond committed Nov 6, 2024
2 parents cebc954 + fb1cb1d commit b1e33f5
Show file tree
Hide file tree
Showing 10 changed files with 129 additions and 44 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ Bug fixes:
* Fix Hash value omission for constant names (@andrykonchin).
* Fix `MatchData#[index, length]` when index is larger than number of matched values (@andrykonchin).
* Fix `#each` for a foreign iterator which is also iterable (#3630, @eregon).
* Fix `Module#name` called inside the `Module#const_added` callback when the module is defined in the top-level scope (#3683, @andrykonchin).
* Fix duplicated calls of a `Module#const_added` callback when a module with nested modules is assigned to a constant (@andrykonchin).

Compatibility:

Expand Down
43 changes: 43 additions & 0 deletions spec/ruby/core/module/const_added_spec.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require_relative '../../spec_helper'
require_relative 'fixtures/classes'
require_relative 'fixtures/const_added'

describe "Module#const_added" do
ruby_version_is "3.2" do
Expand Down Expand Up @@ -63,6 +64,27 @@ module SubModule
ScratchPad.recorded.should == [:SubModule]
end

it "is called when a new module is defined under a named module (assigned to a constant)" do
ScratchPad.record []

ModuleSpecs::ConstAddedSpecs::NamedModule = Module.new do
def self.const_added(name)
ScratchPad << name
end

module self::A
def self.const_added(name)
ScratchPad << name
end

module self::B
end
end
end

ScratchPad.recorded.should == [:A, :B]
end

it "is called when a new class is defined under self" do
ScratchPad.record []

Expand All @@ -83,6 +105,27 @@ class SubClass
ScratchPad.recorded.should == [:SubClass]
end

it "is called when a new class is defined under a named module (assigned to a constant)" do
ScratchPad.record []

ModuleSpecs::ConstAddedSpecs::NamedModuleB = Module.new do
def self.const_added(name)
ScratchPad << name
end

class self::A
def self.const_added(name)
ScratchPad << name
end

class self::B
end
end
end

ScratchPad.recorded.should == [:A, :B]
end

it "is called when an autoload is defined" do
ScratchPad.record []

Expand Down
4 changes: 4 additions & 0 deletions spec/ruby/core/module/fixtures/const_added.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module ModuleSpecs
module ConstAddedSpecs
end
end
3 changes: 3 additions & 0 deletions spec/ruby/core/module/fixtures/name.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,7 @@ def name
.name
end
end

module NameSpecs
end
end
41 changes: 41 additions & 0 deletions spec/ruby/core/module/name_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,47 @@ module ModuleSpecs::Anonymous
valid_names.should include(m::N.name) # You get one of the two, but you don't know which one.
end

ruby_version_is "3.2" do
it "is set in #const_added callback when a module defined in the top-level scope" do
ruby_exe(<<~RUBY, args: "2>&1").chomp.should == "TEST1\nTEST2"
class Module
def const_added(name)
puts const_get(name).name
end
end
# module with name
module TEST1
end
# anonymous module
TEST2 = Module.new
RUBY
end

it "is set in #const_added callback for a nested module when an outer module defined in the top-level scope" do
ScratchPad.record []

ModuleSpecs::NameSpecs::NamedModule = Module.new do
def self.const_added(name)
ScratchPad << const_get(name).name
end

module self::A
def self.const_added(name)
ScratchPad << const_get(name).name
end

module self::B
end
end
end

ScratchPad.recorded.should.one?(/#<Module.+>::A$/)
ScratchPad.recorded.should.one?(/#<Module.+>::A::B$/)
end
end

it "returns a frozen String" do
ModuleSpecs.name.should.frozen?
end
Expand Down
1 change: 1 addition & 0 deletions spec/tags/core/module/name_tags.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
fails:Module#name is not nil when assigned to a constant in an anonymous module
slow:Module#name is set in #const_added callback when a module defined in the top-level scope
10 changes: 5 additions & 5 deletions src/main/java/org/truffleruby/core/CoreLibrary.java
Original file line number Diff line number Diff line change
Expand Up @@ -299,11 +299,11 @@ public CoreLibrary(RubyContext context, RubyLanguage language) {
objectClass = (RubyClass) moduleClass.superclass;
basicObjectClass = (RubyClass) objectClass.superclass;

// Set constants in Object and lexical parents
classClass.fields.getAdoptedByLexicalParent(context, objectClass, "Class", node);
basicObjectClass.fields.getAdoptedByLexicalParent(context, objectClass, "BasicObject", node);
objectClass.fields.getAdoptedByLexicalParent(context, objectClass, "Object", node);
moduleClass.fields.getAdoptedByLexicalParent(context, objectClass, "Module", node);
// Set constants in Object
objectClass.fields.setConstant(context, node, "Class", classClass);
objectClass.fields.setConstant(context, node, "BasicObject", basicObjectClass);
objectClass.fields.setConstant(context, node, "Object", objectClass);
objectClass.fields.setConstant(context, node, "Module", moduleClass);

// Create Exception classes

Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/truffleruby/core/klass/ClassNodes.java
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ private static RubyClass createRubyClass(RubyContext context,
superclass);

if (lexicalParent != null) {
rubyClass.fields.getAdoptedByLexicalParent(context, lexicalParent, name, currentNode);
lexicalParent.fields.setConstant(context, currentNode, name, rubyClass);
}

return rubyClass;
Expand Down
65 changes: 28 additions & 37 deletions src/main/java/org/truffleruby/core/module/ModuleFields.java
Original file line number Diff line number Diff line change
Expand Up @@ -165,53 +165,34 @@ public void afterConstructed() {
getName();
}

public RubyConstant getAdoptedByLexicalParent(
private void nameModule(
RubyContext context,
RubyModule lexicalParent,
String name,
Node currentNode) {
String name) {
assert name != null;

RubyConstant previous = lexicalParent.fields.setConstantInternal(
context,
currentNode,
name,
rubyModule,
false);

if (!hasFullName()) {
// Tricky, we need to compare with the Object class, but we only have a Class at hand.
final RubyClass classClass = getLogicalClass().getLogicalClass();
final RubyClass objectClass = (RubyClass) ((RubyClass) classClass.superclass).superclass;

if (lexicalParent == objectClass) {
if (lexicalParent == getObjectClass()) {
this.setFullName(name);
updateAnonymousChildrenModules(context);
nameChildrenModules(context);
} else if (lexicalParent.fields.hasFullName()) {
this.setFullName(lexicalParent.fields.getName() + "::" + name);
updateAnonymousChildrenModules(context);
nameChildrenModules(context);
} else {
// Our lexicalParent is also an anonymous module
// and will name us when it gets named via updateAnonymousChildrenModules(),
// or we'll compute an anonymous name on #getName() if needed
}
}
return previous;
}

public void updateAnonymousChildrenModules(RubyContext context) {
private void nameChildrenModules(RubyContext context) {
for (Map.Entry<String, ConstantEntry> entry : constants.entrySet()) {
ConstantEntry constantEntry = entry.getValue();
RubyConstant constant = constantEntry.getConstant();
if (constant != null && constant.hasValue() && constant.getValue() instanceof RubyModule) {
RubyModule module = (RubyModule) constant.getValue();
if (!module.fields.hasFullName()) {
module.fields.getAdoptedByLexicalParent(
context,
rubyModule,
entry.getKey(),
null);
}

if (constant != null && constant.hasValue() && constant.getValue() instanceof RubyModule childModule) {
childModule.fields.nameModule(context, rubyModule, entry.getKey());
}
}
}
Expand Down Expand Up @@ -422,15 +403,13 @@ private List<RubyModule> getPrependedModulesAndSelf() {
/** Set the value of a constant, possibly redefining it. */
@TruffleBoundary
public RubyConstant setConstant(RubyContext context, Node currentNode, String name, Object value) {
if (value instanceof RubyModule) {
return ((RubyModule) value).fields.getAdoptedByLexicalParent(
context,
rubyModule,
name,
currentNode);
} else {
return setConstantInternal(context, currentNode, name, value, false);
// A module fully qualified name should replace a temporary one before assigning a constant,
// and before calling in the #const_added callback.
if (value instanceof RubyModule childModule) {
childModule.fields.nameModule(context, rubyModule, name);
}

return setConstantInternal(context, currentNode, name, value, false);
}

@TruffleBoundary
Expand Down Expand Up @@ -785,7 +764,11 @@ public Object getRubyStringName() {
@TruffleBoundary
private String createAnonymousName() {
if (givenBaseName != null) {
return lexicalParent.fields.getName() + "::" + givenBaseName;
if (lexicalParent == getObjectClass()) {
return givenBaseName;
} else {
return lexicalParent.fields.getName() + "::" + givenBaseName;
}
} else if (getLogicalClass() == rubyModule) { // For the case of class Class during initialization
return "#<cyclic>";
} else if (RubyGuards.isSingletonClass(rubyModule)) {
Expand Down Expand Up @@ -1174,4 +1157,12 @@ private void refinedMethod(RubyContext context, String methodName, Node currentN
}
}

private RubyClass getObjectClass() {
// Tricky, we need to compare with the Object class, but we only have a Module at hand.
final RubyClass classClass = getLogicalClass().getLogicalClass();
final RubyClass objectClass = (RubyClass) ((RubyClass) classClass.superclass).superclass;
assert objectClass.fields.givenBaseName == "Object";
return objectClass;
}

}
2 changes: 1 addition & 1 deletion src/main/java/org/truffleruby/core/module/ModuleNodes.java
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,7 @@ public static RubyModule createModule(RubyContext context, SourceSection sourceS
module.fields.afterConstructed();

if (lexicalParent != null) {
module.fields.getAdoptedByLexicalParent(context, lexicalParent, name, currentNode);
lexicalParent.fields.setConstant(context, currentNode, name, module);
}
return module;
}
Expand Down

0 comments on commit b1e33f5

Please sign in to comment.