Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Restrict exp.@var to types in the same hierarchy #6358

Merged

Conversation

asterite
Copy link
Member

@asterite asterite commented Jul 9, 2018

Fixes #6066

With this PR, doing exp.@var only works if:

  1. the current type is the same as the type of exp, or
  2. the current type inherits the type of exp, or
  3. the type of exp inherits the current type, or
  4. the current type and the type of exp live in the same namespace

So, freely accessing instance vars is disallowed. Of course, one can always reopen a class and add an accessor (which is why I didn't see the current behaviour as a big problem, and why probably Ruby has instance_variable_get, because one can always access an instance var if you want to). But, at least we are not encouraging its use.

I was hesitant about point 4. All 4 points mean (Crystal, not Ruby) protected. Without 4, it's almost protected. At first I implemented just 1, 2 and 3, but then noticed it's used in Hash::BaseIterator:

@current = @hash.@first

So I added rule 4. However, later I noticed it's the only place where that "in the same namespace" rule is used for instance vars, at least in the standard library.

But, I think accessing instance vars like that should be mainly used for struct comparison, or accessing instance vars from types in the same hierarchy. If you really need to access an instance var from an unrelated type, even in the same namespace, you can always define a protected method, which gives you that access (like I did in this PR).

I was also hesitant to remove point 4, because now you can't say "exp.@var has protected access", but I expect a doc page explaining those three points without mentioning protected access, and we are done. Plus, it's a super rare thing to need, so understanding this rule and how it's different from protected won't be needed for most of users.

Separate notes:

  • I had to add IO#has_non_utf8_encoding? as :nodoc: because Char#to_s uses it. The other alternative is to do io.encoding == "UTF-8" but that's probably much slower.
  • For specs, I added accessors like _var to test the value of some instance vars. These are necessary because the tests check implementation details.

@jkthorne
Copy link
Contributor

Is this technically a breaking change?

@jkthorne
Copy link
Contributor

Would this prevent access from a sample like this?

require "http/client"

class SampleConnection
  def self.collect_metrics
    client = HTTP::Client.new "www.example.com"
    pp! client.@socket
  end
end

SampleConnection.collect_metrics

@asterite
Copy link
Member Author

Yes, of course.

@jkthorne
Copy link
Contributor

Should there be a test case for not in the top level?

it "can read from any non top-level" do
  assert_error %(
    class Foo
      @x = 1
    end

    class Bar
      def baz
        Foo.new.@x
      end
    end

    Bar.new.baz
    ),
    "can't access Foo.@x from the top-level"
end

Also is there a reason why top level is excluded?

@asterite
Copy link
Member Author

There's a test for that. And your test is accessing the instance var from Bar, not the top level.

@jkthorne
Copy link
Contributor

I just wanted to check. It seems a lot of the tests are passing in params. It is good to know that will be there.

"can't access Bar.@x from unrelated type Foo"
end

it "can't read from top-level" do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason why the top-level is excluded from accessing ivars?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's the main thing this PR changes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am sorry I missed that from the issue. I didnt realize it was restricted like that.

@RX14 RX14 added this to the Next milestone Jul 28, 2018
@RX14 RX14 merged commit 87781a9 into crystal-lang:master Jul 28, 2018
@straight-shoota
Copy link
Member

I was under the impression that the tenor in #6066 was not to change accessing instance variables.

@ysbaddaden
Copy link
Contributor

ysbaddaden commented Jul 30, 2018

  1. stdlib is broken in some places, for example: My bad: "old" branch checkout.
in src/char.cr:773: can't access String::Builder.@encoding from unrelated type Char
      if io.@encoding
  1. why was this merged when discussion in Avoid ivars to be read on objects without getter #6066 was leaning towards "keep current behavior" ?!

@RX14 RX14 modified the milestones: Next, 0.26.0 Jul 30, 2018
@jkthorne
Copy link
Contributor

jkthorne commented Aug 3, 2018

Following up from earlier. I am curious why was this merged?

@jkthorne jkthorne mentioned this pull request Aug 3, 2018
@bcardiff
Copy link
Member

bcardiff commented Aug 3, 2018

It doesn’t follow from discussion that this should have been merged. Let’s revert this PR before the release :-)

@RX14
Copy link
Contributor

RX14 commented Aug 3, 2018

Better to try it and see what it breaks in my opinion. We can always revert it in a point release if people actually need this feature.

This PR makes the code cleaner at least, so please avoid doing a flat-out git revert and reverting the cleanups too.

@straight-shoota
Copy link
Member

@RX14 Breaking it to see if users' code breaks while we actually don't want it to break (and break again in a point release) doesn't seem like a good idea at all.

@RX14
Copy link
Contributor

RX14 commented Aug 4, 2018

@straight-shoota we change things to see if they break all the time, look at #6024, which was reverted.

Let's do some science.

@straight-shoota
Copy link
Member

Yeah, but that change was intended. It would have stayed if it didn't break.

This one is, as far as I am concerned, not intended. So why bother breaking if we don't want it in the end anyways?

bcardiff pushed a commit to bcardiff/crystal that referenced this pull request Aug 4, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

7 participants