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

extend cases of unnecessary_null_checks/null_check_on_nullable_type_parameter #3258

Merged
merged 6 commits into from
Mar 15, 2022

Conversation

a14n
Copy link
Contributor

@a14n a14n commented Feb 28, 2022

Description

This PR extends the cases where unnecessary_null_checks and null_check_on_nullable_type_parameter apply:

  • in yield expressions
  • in list/set/map literals
  • in await expression

Related to #3256

/cc @srawlins

@coveralls
Copy link

coveralls commented Feb 28, 2022

Coverage Status

Coverage increased (+0.07%) to 95.681% when pulling 545ba5c on a14n:extend-null-checks into 8c01f68 on dart-lang:master.

@a14n
Copy link
Contributor Author

a14n commented Mar 3, 2022

@pq is there something blocking here?

DartType? Function(DartType? type) mayHandleAwait = (type) => type;
if (parent is AwaitExpression) {
mayHandleAwait =
(type) => (type as ParameterizedType?)?.typeArguments.first;
Copy link
Member

Choose a reason for hiding this comment

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

I might be misunderstanding the code, but if the goal of this is to unwrap Future and FutureOr, it will actually attempt unwrap any potentially parameterized type. (The class ParameterizedType only means "a kind of type that is allowed to have type parameters". It doesn't mean "a type that actually has type parameters".)

I think that this will happily unwrap List<int>, and will crash when type is something without type arguments, such as String.

Consider using isDartAsyncFuture and isDartAsyncFutureOr explicitly.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

humm, there was indeed issue with await handling. The return await case is the only one that need a special processing because the target type is really a Future (unlike other case where the unwrap type is provided)

return null;
}
return mayHandleAwait(
(staticType.returnType as ParameterizedType).typeArguments.first);
Copy link
Member

Choose a reason for hiding this comment

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

This and a few places below have the same problem. Consider moving the logic into a single _unwrapFuture method / function.

if (staticType is! FunctionType) {
return null;
}
return (staticType.returnType as ParameterizedType).typeArguments.first;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Same here. Having a yield means that the return type is either Iterable<X> or Stream<X>. So a ParameterizedType.

Copy link
Member

Choose a reason for hiding this comment

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

No, having a yield means that the return type is a supertype of one of those types. dynamic works just fine and it isn't a ParameterizedType.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

my bad, sorry and thanks

@pq
Copy link
Member

pq commented Mar 3, 2022

@pq is there something blocking here?

Sorry was tangled up elsewhere. The big thing is to make sure these lints aren't being used in places where new diagnostics would block an SDK roll. I don't see them used internally on a quick scan.

I do see them enabled in flutter... Do you know if this will flag new cases there?

return null;
}
if (withAwait || parentExpression.body.keyword?.lexeme == 'async') {
// return type is obligatorily a Future<X>
Copy link
Member

Choose a reason for hiding this comment

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

The return type could also be void, dynamic, or Never, so it isn't safe to just cast it to a ParameterizedType; doing so could cause an exception to be thrown.

Even when it's a ParameterizedType, you can't assume that it has type arguments. Every InterfaceType is a ParameterizedType, but not every InterfaceType has type arguments. So asking for the first type argument without first checking that the list isn't empty will result in an exception being thrown.

Copy link
Member

Choose a reason for hiding this comment

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

Maybe a host of negative tests would be good here?

if (staticType is! FunctionType) {
return null;
}
return (staticType.returnType as ParameterizedType).typeArguments.first;
Copy link
Member

Choose a reason for hiding this comment

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

No, having a yield means that the return type is a supertype of one of those types. dynamic works just fine and it isn't a ParameterizedType.

@a14n
Copy link
Contributor Author

a14n commented Mar 3, 2022

I do see them enabled in flutter... Do you know if this will flag new cases there?

I see only one additional lint fixed by flutter/flutter#99507

@a14n
Copy link
Contributor Author

a14n commented Mar 14, 2022

flutter/flutter#99507 is now merged.

PTAL

Copy link
Member

@bwilkerson bwilkerson left a comment

Choose a reason for hiding this comment

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

It would be good to have some tests for the cases that the code is guarding against in order to ensure that it is correct.

Also, it didn't occur to me last time, but do we need to worry about subclasses of the targeted types? For example:

class Source extends Iterable<int?> {}

Source f16(int? p) async => p!; // LINT?

What about type aliases:

typedef Source = Iterable<int?>;

If so, we would need to use DartType.asInstanceOf to find the actual value of the type argument (and wouldn't need the isDartCore* tests).

@a14n
Copy link
Contributor Author

a14n commented Mar 14, 2022

Also, it didn't occur to me last time, but do we need to worry about subclasses of the targeted types? For example:

class Source extends Iterable<int?> {}

Source f16(int? p) async => p!; // LINT?

Sorry but I don't understand this example.

What about type aliases:

typedef Source = Iterable<int?>;

If so, we would need to use DartType.asInstanceOf to find the actual value of the type argument (and wouldn't need the isDartCore* tests).

I added a test without any change and it seems to work correctly:

typedef F16 = Future<int?>;
F16 f16(int? p) async => p!; // LINT

Copy link
Member

@bwilkerson bwilkerson left a comment

Choose a reason for hiding this comment

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

Sorry but I don't understand this example.

I didn't realize that wasn't valid. Sorry for the noise.

Still wouldn't hurt to have some negative tests for the cases we're explicitly guarding against.

@pq pq merged commit de38ee4 into dart-lang:master Mar 15, 2022
@a14n a14n deleted the extend-null-checks branch March 16, 2022 07:10
copybara-service bot pushed a commit to dart-lang/sdk that referenced this pull request Aug 23, 2023
…arameter (dart-lang/linter#3258)

* extend cases of unnecessary_null_checks/null_check_on_nullable_type_parameter

* fix bad await handling

* add handling of async

* address review comments

* add test for typedef

* add some negative tests
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants