Skip to content

Commit 9b53501

Browse files
committed
[GR-18163] Warn when a global variable is not initialized
PullRequest: truffleruby/3382
2 parents 4df60b6 + 342d70a commit 9b53501

File tree

6 files changed

+154
-6
lines changed

6 files changed

+154
-6
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Bug fixes:
1010
Compatibility:
1111

1212
* Fix `Array#fill` to raise `TypeError` instead of `ArgumentError` when the length argument is not numeric (#2652, @andrykonchin).
13+
* Warn when a global variable is not initialized (#2595, @andrykonchin).
1314

1415
Performance:
1516

spec/ruby/language/predefined_spec.rb

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -570,7 +570,6 @@ def foo
570570
($/ = "xyz").should == "xyz"
571571
end
572572

573-
574573
it "changes $-0" do
575574
$/ = "xyz"
576575
$-0.should equal($/)
@@ -641,6 +640,45 @@ def foo
641640
end
642641
end
643642

643+
describe "Predefined global $\\" do
644+
before :each do
645+
@verbose, $VERBOSE = $VERBOSE, nil
646+
@dollar_backslash = $\
647+
end
648+
649+
after :each do
650+
$\ = @dollar_backslash
651+
$VERBOSE = @verbose
652+
end
653+
654+
it "can be assigned a String" do
655+
str = "abc"
656+
$\ = str
657+
$\.should equal(str)
658+
end
659+
660+
it "can be assigned nil" do
661+
$\ = nil
662+
$\.should be_nil
663+
end
664+
665+
it "returns the value assigned" do
666+
($\ = "xyz").should == "xyz"
667+
end
668+
669+
it "does not call #to_str to convert the object to a String" do
670+
obj = mock("$\\ value")
671+
obj.should_not_receive(:to_str)
672+
673+
-> { $\ = obj }.should raise_error(TypeError)
674+
end
675+
676+
it "raises a TypeError if assigned not String" do
677+
-> { $\ = 1 }.should raise_error(TypeError)
678+
-> { $\ = true }.should raise_error(TypeError)
679+
end
680+
end
681+
644682
describe "Predefined global $," do
645683
after :each do
646684
$, = nil
@@ -1340,3 +1378,29 @@ def obj.foo2; yield; end
13401378
end
13411379
end
13421380
end
1381+
1382+
# Some other pre-defined global variables
1383+
1384+
describe "Predefined global $=" do
1385+
before :each do
1386+
@verbose, $VERBOSE = $VERBOSE, nil
1387+
@dollar_assign = $=
1388+
end
1389+
1390+
after :each do
1391+
$= = @dollar_assign
1392+
$VERBOSE = @verbose
1393+
end
1394+
1395+
it "warns when accessed" do
1396+
-> { a = $= }.should complain(/is no longer effective/)
1397+
end
1398+
1399+
it "warns when assigned" do
1400+
-> { $= = "_" }.should complain(/is no longer effective/)
1401+
end
1402+
1403+
it "returns the value assigned" do
1404+
($= = "xyz").should == "xyz"
1405+
end
1406+
end

spec/ruby/language/variables_spec.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -849,4 +849,22 @@ def obj.foobar; @a ||= 42; end
849849
-> { obj.foobar }.should_not complain(verbose: true)
850850
end
851851
end
852+
853+
describe "global variable" do
854+
context "when global variable is uninitialized" do
855+
it "warns about accessing uninitialized global variable in verbose mode" do
856+
obj = Object.new
857+
def obj.foobar; a = $specs_uninitialized_global_variable; end
858+
859+
-> { obj.foobar }.should complain(/warning: global variable `\$specs_uninitialized_global_variable' not initialized/, verbose: true)
860+
end
861+
862+
it "doesn't warn at lazy initialization" do
863+
obj = Object.new
864+
def obj.foobar; $specs_uninitialized_global_variable_lazy ||= 42; end
865+
866+
-> { obj.foobar }.should_not complain(verbose: true)
867+
end
868+
end
869+
end
852870
end

src/main/java/org/truffleruby/language/globals/GlobalVariableStorage.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121

2222
public final class GlobalVariableStorage {
2323

24-
private static final Object UNSET_VALUE = NotProvided.INSTANCE;
24+
public static final Object UNSET_VALUE = NotProvided.INSTANCE;
2525

2626
private final CyclicAssumption unchangedAssumption = new CyclicAssumption("global variable unchanged");
2727
private int changes = 0;
@@ -53,6 +53,10 @@ public Object getValue() {
5353
return currentValue == UNSET_VALUE ? Nil.INSTANCE : currentValue;
5454
}
5555

56+
public Object getRawValue() {
57+
return value;
58+
}
59+
5660
public boolean isDefined() {
5761
return value != UNSET_VALUE;
5862
}

src/main/java/org/truffleruby/language/globals/ReadSimpleGlobalVariableNode.java

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,15 @@
1010
package org.truffleruby.language.globals;
1111

1212
import com.oracle.truffle.api.CompilerDirectives;
13+
import com.oracle.truffle.api.profiles.ConditionProfile;
14+
import com.oracle.truffle.api.source.SourceSection;
15+
import org.truffleruby.core.string.StringUtils;
16+
import org.truffleruby.language.Nil;
1317
import org.truffleruby.language.RubyBaseNode;
1418

1519
import com.oracle.truffle.api.dsl.Cached;
1620
import com.oracle.truffle.api.dsl.Specialization;
21+
import org.truffleruby.language.WarningNode;
1722

1823
public abstract class ReadSimpleGlobalVariableNode extends RubyBaseNode {
1924

@@ -38,19 +43,50 @@ public ReadSimpleGlobalVariableNode(String name) {
3843
protected Object readConstant(
3944
@Cached("getLanguage().getGlobalVariableIndex(name)") int index,
4045
@Cached("getContext().getGlobalVariableStorage(index)") GlobalVariableStorage storage,
41-
@Cached("storage.getValue()") Object value) {
46+
@Cached("storage.getValue()") Object value,
47+
@Cached("new()") WarningNode warningNode,
48+
@Cached("storage.isDefined()") boolean isDefined) {
49+
if (!isDefined && warningNode.shouldWarn()) {
50+
SourceSection sourceSection = getEncapsulatingSourceSection();
51+
String message = globalVariableNotInitializedMessageFor(name);
52+
53+
warningNode.warningMessage(sourceSection, message);
54+
}
55+
4256
return value;
4357
}
4458

4559
@Specialization
46-
protected Object read() {
60+
protected Object read(
61+
@Cached("new()") WarningNode warningNode,
62+
@Cached ConditionProfile isDefinedProfile) {
4763
if (lookupGlobalVariableStorageNode == null) {
4864
CompilerDirectives.transferToInterpreterAndInvalidate();
4965
lookupGlobalVariableStorageNode = insert(LookupGlobalVariableStorageNode.create(name));
5066
}
67+
5168
final GlobalVariableStorage storage = lookupGlobalVariableStorageNode.execute(null);
5269

53-
return storage.getValue();
70+
// don't use storage.getValue() and storage.isDefined() to avoid
71+
// accessing volatile storage.value several times
72+
final Object rawValue = storage.getRawValue();
73+
74+
if (isDefinedProfile.profile(rawValue != GlobalVariableStorage.UNSET_VALUE)) {
75+
return rawValue;
76+
} else {
77+
if (warningNode.shouldWarn()) {
78+
SourceSection sourceSection = getEncapsulatingSourceSection();
79+
String message = globalVariableNotInitializedMessageFor(name);
80+
81+
warningNode.warningMessage(sourceSection, message);
82+
}
83+
84+
return Nil.INSTANCE;
85+
}
86+
}
87+
88+
private String globalVariableNotInitializedMessageFor(String name) {
89+
return StringUtils.format("global variable `%s' not initialized", name);
5490
}
5591

5692
}

src/main/ruby/truffleruby/core/truffle/kernel_operations.rb

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,18 @@ def $LOAD_PATH.resolve_feature_path(file_name)
8585

8686
$/ = "\n".freeze
8787

88+
define_hooked_variable(
89+
:$\,
90+
-> { Primitive.global_variable_get :$\ },
91+
-> v {
92+
if v && !Primitive.object_kind_of?(v, String)
93+
raise TypeError, '$\ must be a String'
94+
end
95+
Primitive.global_variable_set :$\, v
96+
})
97+
98+
$\ = nil
99+
88100
Truffle::Boot.delay do
89101
if Truffle::Boot.get_option 'chomp-loop'
90102
$\ = $/
@@ -121,7 +133,18 @@ def $LOAD_PATH.resolve_feature_path(file_name)
121133

122134
$, = nil # It should be defined by the time boot has finished.
123135

124-
$= = false
136+
define_hooked_variable(
137+
:$=,
138+
-> {
139+
warn 'variable $= is no longer effective', uplevel: 1 if Warning[:deprecated]
140+
Primitive.global_variable_get :$=
141+
},
142+
-> v {
143+
warn 'variable $= is no longer effective', uplevel: 1 if Warning[:deprecated]
144+
Primitive.global_variable_set :$=, v
145+
})
146+
147+
Primitive.global_variable_set :$=, false
125148

126149
define_hooked_variable(
127150
:$VERBOSE,
@@ -173,6 +196,8 @@ def $LOAD_PATH.resolve_feature_path(file_name)
173196
Primitive.global_variable_set :"$;", v
174197
})
175198

199+
$; = nil
200+
176201
def self.load_error(name)
177202
load_error = LoadError.new("cannot load such file -- #{name}")
178203
load_error.path = name

0 commit comments

Comments
 (0)