diff --git a/core/module/const_source_location_spec.rb b/core/module/const_source_location_spec.rb new file mode 100644 index 0000000000..757d831708 --- /dev/null +++ b/core/module/const_source_location_spec.rb @@ -0,0 +1,227 @@ +require_relative '../../spec_helper' +require_relative '../../fixtures/constants' +require_relative 'fixtures/constants_autoload' + +describe "Module#const_source_location" do + before do + @constants_fixture_path = File.expand_path('../../fixtures/constants.rb', __dir__) + end + + ruby_version_is "2.7" do + # NOTE: please keep this specs on the top to avoid breaking specs + describe "with dynamically assigned constants" do + it "searches a path in the immediate class or module first" do + ConstantSpecs::ClassA::CS_CONST301 = :const301_1 + ConstantSpecs::ClassA.const_source_location(:CS_CONST301).should == [__FILE__, 14] + + ConstantSpecs::ModuleA::CS_CONST301 = :const301_2 + ConstantSpecs::ModuleA.const_source_location(:CS_CONST301).should == [__FILE__, 17] + + ConstantSpecs::ParentA::CS_CONST301 = :const301_3 + ConstantSpecs::ParentA.const_source_location(:CS_CONST301).should == [__FILE__, 20] + + ConstantSpecs::ContainerA::ChildA::CS_CONST301 = :const301_5 + ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST301).should == [__FILE__, 23] + end + + it "searches a path in a module included in the immediate class before the superclass" do + ConstantSpecs::ParentB::CS_CONST302 = :const302_1 + ConstantSpecs::ModuleF::CS_CONST302 = :const302_2 + ConstantSpecs::ContainerB::ChildB.const_source_location(:CS_CONST302).should == [__FILE__, 29] + end + + it "searches a path in the superclass before a module included in the superclass" do + ConstantSpecs::ModuleE::CS_CONST303 = :const303_1 + ConstantSpecs::ParentB::CS_CONST303 = :const303_2 + ConstantSpecs::ContainerB::ChildB.const_source_location(:CS_CONST303).should == [__FILE__, 35] + end + + it "searches a path in a module included in the superclass" do + ConstantSpecs::ModuleA::CS_CONST304 = :const304_1 + ConstantSpecs::ModuleE::CS_CONST304 = :const304_2 + ConstantSpecs::ContainerB::ChildB.const_source_location(:CS_CONST304).should == [__FILE__, 41] + end + + it "searches a path in the superclass chain" do + ConstantSpecs::ModuleA::CS_CONST305 = :const305 + ConstantSpecs::ContainerB::ChildB.const_source_location(:CS_CONST305).should == [__FILE__, 46] + end + + it "returns path to a toplevel constant when the receiver is a Class" do + Object::CS_CONST306 = :const306 + ConstantSpecs::ContainerB::ChildB.const_source_location(:CS_CONST306).should == [__FILE__, 51] + end + + it "returns path to a toplevel constant when the receiver is a Module" do + Object::CS_CONST308 = :const308 + ConstantSpecs.const_source_location(:CS_CONST308).should == [__FILE__, 56] + ConstantSpecs::ModuleA.const_source_location(:CS_CONST308).should == [__FILE__, 56] + end + + it "returns path to the updated value of a constant" do + ConstantSpecs::ClassB::CS_CONST309 = :const309_1 + ConstantSpecs::ClassB.const_source_location(:CS_CONST309).should == [__FILE__, 62] + + -> { + ConstantSpecs::ClassB::CS_CONST309 = :const309_2 + }.should complain(/already initialized constant/) + + ConstantSpecs::ClassB.const_source_location(:CS_CONST309).should == [__FILE__, 66] + end + end + + describe "with statically assigned constants" do + it "searches location path the immediate class or module first" do + ConstantSpecs::ClassA.const_source_location(:CS_CONST10).should == [@constants_fixture_path, 78] + ConstantSpecs::ModuleA.const_source_location(:CS_CONST10).should == [@constants_fixture_path, 36] + ConstantSpecs::ParentA.const_source_location(:CS_CONST10).should == [@constants_fixture_path, 100] + ConstantSpecs::ContainerA.const_source_location(:CS_CONST10).should == [@constants_fixture_path, 120] + ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST10).should == [@constants_fixture_path, 137] + end + + it "searches location path a module included in the immediate class before the superclass" do + ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST15).should == [@constants_fixture_path, 52] + end + + it "searches location path the superclass before a module included in the superclass" do + ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST11).should == [@constants_fixture_path, 101] + end + + it "searches location path a module included in the superclass" do + ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST12).should == [@constants_fixture_path, 46] + end + + it "searches location path the superclass chain" do + ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST13).should == [@constants_fixture_path, 38] + end + + it "returns location path a toplevel constant when the receiver is a Class" do + ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST1).should == [@constants_fixture_path, 30] + end + + it "returns location path a toplevel constant when the receiver is a Module" do + ConstantSpecs.const_source_location(:CS_CONST1).should == [@constants_fixture_path, 30] + ConstantSpecs::ModuleA.const_source_location(:CS_CONST1).should == [@constants_fixture_path, 30] + end + end + + it "return empty path if constant defined in C code" do + Object.const_source_location(:String).should == [] + end + + it "accepts a String or Symbol name" do + Object.const_source_location(:CS_CONST1).should == [@constants_fixture_path, 30] + Object.const_source_location("CS_CONST1").should == [@constants_fixture_path, 30] + end + + it "returns nil if no constant is defined in the search path" do + ConstantSpecs.const_source_location(:CS_CONSTX).should == nil + end + + it "raises a NameError if the name does not start with a capital letter" do + -> { ConstantSpecs.const_source_location "name" }.should raise_error(NameError) + end + + it "raises a NameError if the name starts with a non-alphabetic character" do + -> { ConstantSpecs.const_source_location "__CONSTX__" }.should raise_error(NameError) + -> { ConstantSpecs.const_source_location "@CS_CONST1" }.should raise_error(NameError) + -> { ConstantSpecs.const_source_location "!CS_CONST1" }.should raise_error(NameError) + end + + it "raises a NameError if the name contains non-alphabetic characters except '_'" do + Object.const_source_location("CS_CONST1").should == [@constants_fixture_path, 30] + -> { ConstantSpecs.const_source_location "CS_CONST1=" }.should raise_error(NameError) + -> { ConstantSpecs.const_source_location "CS_CONST1?" }.should raise_error(NameError) + end + + it "calls #to_str to convert the given name to a String" do + name = mock("ClassA") + name.should_receive(:to_str).and_return("ClassA") + ConstantSpecs.const_source_location(name).should == [@constants_fixture_path, 77] + end + + it "raises a TypeError if conversion to a String by calling #to_str fails" do + name = mock('123') + -> { ConstantSpecs.const_source_location(name) }.should raise_error(TypeError) + + name.should_receive(:to_str).and_return(123) + -> { ConstantSpecs.const_source_location(name) }.should raise_error(TypeError) + end + + it "does not search the singleton class of a Class or Module" do + ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST14).should == nil + ConstantSpecs.const_source_location(:CS_CONST14).should == nil + end + + it "does not search the containing scope" do + ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST20).should == [@constants_fixture_path, 103] + ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST5) == nil + end + + it "returns nil if the constant is defined in the receiver's superclass and the inherit flag is false" do + ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST4, false).should == nil + end + + it "searches into the receiver superclasses if the inherit flag is true" do + ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST4, true).should == [@constants_fixture_path, 99] + end + + it "returns nil when the receiver is a Module, the constant is defined at toplevel and the inherit flag is false" do + ConstantSpecs::ModuleA.const_source_location(:CS_CONST1, false).should == nil + end + + it "returns nil when the receiver is a Class, the constant is defined at toplevel and the inherit flag is false" do + ConstantSpecs::ContainerA::ChildA.const_source_location(:CS_CONST1, false).should == nil + end + + it "accepts a toplevel scope qualifier" do + ConstantSpecs.const_source_location("::CS_CONST1").should == [@constants_fixture_path, 30] + end + + it "accepts a scoped constant name" do + ConstantSpecs.const_source_location("ClassA::CS_CONST10").should == [@constants_fixture_path, 78] + end + + it "raises a NameError if the name includes two successive scope separators" do + -> { ConstantSpecs.const_source_location("ClassA::::CS_CONST10") }.should raise_error(NameError) + end + + it "raises a NameError if only '::' is passed" do + -> { ConstantSpecs.const_source_location("::") }.should raise_error(NameError) + end + + it "raises a NameError if a Symbol has a toplevel scope qualifier" do + -> { ConstantSpecs.const_source_location(:'::CS_CONST1') }.should raise_error(NameError) + end + + it "raises a NameError if a Symbol is a scoped constant name" do + -> { ConstantSpecs.const_source_location(:'ClassA::CS_CONST10') }.should raise_error(NameError) + end + + it "does search private constants path" do + ConstantSpecs.const_source_location(:CS_PRIVATE).should == [@constants_fixture_path, 284] + end + + context 'autoload' do + before do + @constants_autoload_path = File.expand_path('fixtures/constants_autoload', __dir__) + end + + it 'does search path in autoload declaration' do + Object.const_source_location('CSAutoloadA').should == [@constants_autoload_path + '_a.rb', 1] + end + + it 'does search path in autoload declaration a constant with a toplevel scope qualifier' do + Object.const_source_location('::CSAutoloadB').should == [@constants_autoload_path + '_b.rb', 1] + end + + it 'does search path in autoload declaration a module and resolve a constant within' do + Object.const_source_location('CSAutoloadC::CONST').should == [@constants_autoload_path + '_c.rb', 2] + end + + it 'does autoload a non-toplevel module' do + Object.const_source_location('CSAutoloadD::InnerModule').should == [@constants_autoload_path + '_d.rb', 2] + end + end + end +end