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

New error-prone rule: PreferImmutableStreamExCollections #1670

Merged
merged 13 commits into from
Mar 6, 2021

Conversation

iamdanfox
Copy link
Contributor

@iamdanfox iamdanfox commented Mar 5, 2021

Before this PR

The apollo codebases use StreamEx heavily. As @avisbal and @athorwall have commented in recent CRs, we try to use toImmutableMap where we remember but aren't completely consistent with this.

After this PR

==COMMIT_MSG==
A new error-prone rule PreferImmutableStreamExCollections converts the StreamEx toMap() -> toImmutableMap(), toImmutableList() and toImmutableSet()
==COMMIT_MSG==

Possible downsides?

So if we want to be really picky here, then technically the Collections.unmodifableSet wrapper means we're gonna allocate 1 (one) whole extra object. I don't think this is a blocker, because if someone is already tolerating a whole stream full of lambdas, then I think they can deal with one extra allocation.

    public Set<T> toSet() {
        return rawCollect(Collectors.toSet());
    }

    public Set<T> toImmutableSet() {
        Set<T> result = toSet();
        if (result.size() == 0)
            return Collections.emptySet();
        return Collections.unmodifiableSet(result);
    }

@changelog-app
Copy link

changelog-app bot commented Mar 5, 2021

Generate changelog in changelog/@unreleased

Type

  • Feature
  • Improvement
  • Fix
  • Break
  • Deprecation
  • Manual task
  • Migration

Description

A new error-prone rule PreferImmutableStreamExCollections converts the StreamEx toMap() -> toImmutableMap(), toImmutableList() and toImmutableSet()

Check the box to generate changelog(s)

  • Generate changelog entry

@policy-bot policy-bot bot requested a review from tpetracca March 5, 2021 20:48
@iamdanfox iamdanfox requested a review from carterkozak March 5, 2021 20:48
if (TO_MAP.matches(tree, state)) {
String base = state.getSourceForNode(ASTHelpers.getReceiver(tree.getMethodSelect()));
return buildDescription(tree)
.setMessage("Prefer .toImmutableMap()")
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: this is the only message most people see, I'd exclude overriding the more verbose message from the class definition since that explains the intent.

@@ -71,6 +71,7 @@
// TODO(ckozak): re-enable pending scala check
// "ThrowSpecificity",
"UnsafeGaugeRegistration",
"PreferImmutableStreamExCollections",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

If someone out there is relying on the mutability of their streamex collections, e.g.:

Map<String, String> foo = myStream.toMap();
foo.remove("bar");
return foo;

Then this auto-fix will make a change which still compiles but would throw when they run this. This should get caught by tests, but if they also don't have tests then this could break someone in production.

@Override
public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
if (TO_MAP.matches(tree, state)) {
String base = state.getSourceForNode(ASTHelpers.getReceiver(tree.getMethodSelect()));
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we can avoid a bit of duplication using the SuggestedFixes helpers:

SuggestedFixes.renameMethodInvocation(tree, "toImmutableMap", state)

@@ -71,6 +71,7 @@
// TODO(ckozak): re-enable pending scala check
// "ThrowSpecificity",
"UnsafeGaugeRegistration",
"PreferImmutableStreamExCollections",
Copy link
Contributor

Choose a reason for hiding this comment

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

Limiting to streamex as opposed to standard streams reduces some danger, but I wouldn't be surprised if there are places which expect to mutate the result. I wouldn't want the robots to cause p0s.
It's unfortunate that the toImmutable*() methods don't return a special type like ImmutableList where the type system could help us out. Flow analysis is more difficult.

Copy link
Contributor Author

@iamdanfox iamdanfox Mar 5, 2021

Choose a reason for hiding this comment

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

Yep, this is a theoretical possibility. In practise, I think it's gonna be a very small number of repos who have decided they like the functional style of stream-ex but then rely on mutability (who are these people ?!?!) and they'd also need to have no unit/integration test coverage in order for a release to get out. Moreover we have blue-green deploys which are supposed to minimize the user-facing impact if something slips through the net 🤷

Copy link
Contributor

Choose a reason for hiding this comment

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

What if we don't auto-fix, but update the description to include both a warning about mutability, and instructions to apply the fixes ./gradlew classes -PerrorProneApply=PreferImmutableStreamExCollections

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I think I wanna stick with the autofix here. Immutability is such a core tenet of conjure and the way we write software at Palantir, that I think it's worth ratcheting this towards a better world. This PR should have a pretty minimal scope, because it won't touch the regular usage of java.util.streams.Stream.

Also since this is potentially gonna make code changes, people will have to +1 it before it goes in.

public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) {
if (TO_MAP.matches(tree, state)) {
return buildDescription(tree)
.setMessage("Prefer .toImmutableMap() because immutable collections are inherently threadsafe and"
Copy link
Contributor

Choose a reason for hiding this comment

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

Error-prone automatically uses the message from the class-level annotation if we don't set a message here!

@iamdanfox
Copy link
Contributor Author

So I've intentionally avoided targetting java.util.stream.Stream for now, because I think it's extremely widely used, but perhaps for users of stream-ex I should actually try to zap the regular .collect() methods, e.g.:

-StreamEx.of().collect(Collectors.Set())
+StreamEx.of().collect(Collectors.toUnmodifiableSet())

I'd need to detect if sourceCompatibility was >= 10 somehow...

@carterkozak
Copy link
Contributor

carterkozak commented Mar 5, 2021

Well, if you have StreamEx, you probably already have a guava dependency -- can use MoreCollectors.toImmutable*() in that case (across all source versions)

@iamdanfox
Copy link
Contributor Author

iamdanfox commented Mar 5, 2021

Interestingly I ran ./gradlew classes -PerrorProneApply=PreferImmutableStreamExCollections on a large internal project and it didn't make a single edit... although it seems there was only one mention of EntryStream in the whole project, so maybe stream-ex is just more of a foundry thing??

Got a +500 -500 diff on https://internal-github/f/a/pull/8412 though

@bulldozer-bot bulldozer-bot bot merged commit 3b9f38e into develop Mar 6, 2021
@bulldozer-bot bulldozer-bot bot deleted the dfox/immutable-streamex branch March 6, 2021 00:32
@svc-autorelease
Copy link
Collaborator

Released 3.71.0

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.

3 participants