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

Extract new BuggyObsoleteStrictMemoization cop #175

Merged
merged 3 commits into from Sep 1, 2023

Conversation

amomchilov
Copy link
Contributor

@amomchilov amomchilov commented Aug 31, 2023

The Sorbet/ObsoleteStrictMemoization cop introduced in #162 was initially meant to detect this kind of pattern:

sig { returns(Foo) }
def foo
  @foo = T.let(@foo, T.nilable(Foo))
  @foo ||= Foo.new
end

@Morriar had the fantastic idea to also detect a mistaken variant:

sig { returns(Foo) }
def foo
  # This would have been a mistake, causing the memoized value to be discarded and recomputed on every call.
  @foo = T.let(nil, T.nilable(Foo))
  #            !!!
  @foo ||= Foo.new
end

This was a great idea and found several such bugs in our program, but wasn't implemented quite right.

Auto-correcting this is dangerous. If the computation being memoized (Foo.new, in this case) had side effects, calling it only once (instead of once on every call to foo) can be observed, and might be a breaking change. This is problematic because Sorbet/ObsoleteStrictMemoization is marked Safe: true and SafeAutoCorrect: true.

This PR splits off this behaviour into a new BuggyObsoleteStrictMemoization cop. The output of this cop is the correct (but still obsolete) memoization pattern. From there, ObsoleteStrictMemoization can be applied to bring it to the modern standard. This cop is marked Safe: true, SafeAutoCorrect: false.

“specs” made it sound like it would include all gem specs, including Sorbet.
@amomchilov amomchilov added bugfix Fix a bug feature Add a new feature labels Aug 31, 2023
@amomchilov amomchilov self-assigned this Aug 31, 2023
@amomchilov amomchilov force-pushed the buggy-obsolete-memoization-cop branch from 191faa1 to 4063f7e Compare August 31, 2023 00:20
expect_offense(<<~RUBY)
def foo
@foo = T.let(nil, T.nilable(Foo))
^^^ This might be a mistaken variant of the two-stage workaround that used to be needed for memoization in `#typed: strict` files. See https://sorbet.org/docs/type-assertions#put-type-assertions-behind-memoization.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Kinda nice, the ^^^ is now only under the nil part of this, rather than the whole line.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For an easier reviewing experience, take a look at the commit list.

I made this by first copying over the ObsoleteStrictMemoization as-is, then making changes to it in a second commit. YoSo you can just review the diff to go from ObsoleteStrictMemoization to BuggyObsoleteStrictMemoization

@amomchilov amomchilov marked this pull request as ready for review August 31, 2023 00:40
@amomchilov amomchilov requested a review from a team as a code owner August 31, 2023 00:40
@andyw8
Copy link
Contributor

andyw8 commented Aug 31, 2023

I wonder if it should be named BuggyStrictMemoization, since this would still detect problems even if the Obsolete cop wasn't in use?

@amomchilov
Copy link
Contributor Author

amomchilov commented Aug 31, 2023

@andyw8 I'm torn on it. It's a bit wordier, but I think it helped with clarity and connecting the two cops. That was the name I originally had, but I liked being able to reference the phrase "obsolete memoization pattern" in docs to help explain it.

"strict memoization pattern" wouldn't be as clear, since there's the old 2-line one, and the new one-liner.

$(const {nil? cbase} :T) :let
{(ivar _ivar) nil}
(send (const {nil? cbase} :T) :nilable $_ivar_type))) # T.nilable(_ivar_type)
$(or-asgn (ivasgn _ivar) $_initialization_expr)) # Second line: @_ivar ||= _initialization_expr
Copy link
Contributor

Choose a reason for hiding this comment

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

Should we add a test showing that the cop doesn't trigger if the lines are separated:

@foo = T.let(@foo, T.nilable(Foo))
bar
@foo ||= Foo.new

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good call. Done.

expect_no_offenses(<<~RUBY)
def foo
@foo = T.let(@foo, T.nilable(Foo))
some_other_computation
Copy link
Contributor

Choose a reason for hiding this comment

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

👍

@amomchilov amomchilov merged commit d2bd9e2 into main Sep 1, 2023
10 checks passed
@amomchilov amomchilov deleted the buggy-obsolete-memoization-cop branch September 1, 2023 18:29
@shopify-shipit shopify-shipit bot temporarily deployed to production September 25, 2023 14:39 Inactive
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bugfix Fix a bug feature Add a new feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants