-
Notifications
You must be signed in to change notification settings - Fork 166
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@leafpetersen Is this something that some users are likely to want to enforce? Not having had an opportunity to actually use null safety features yet, it isn't immediately obvious to me.
lib/src/rules/use_late.dart
Outdated
|
||
const _details = r''' | ||
|
||
Use late for private members with non-nullable type to avoid null checks. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we attempting to avoid null checks in order to improve performance or for readability?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mainly readability because I don't have number about perf (as there are less null checks I guess it will be better).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on Erik's comment, it sounds like code should have about the same performance either way. It would be good to clarify in the details that the purpose is to make it clear in the code that a field / variable is not expected to be null
. Users who are new to null safety might not yet understand the value of late
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment updated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Definitely readability.
It makes some checks implicit that you would otherwise have to write boiler-plate code for. That should make the actually important parts of the code stand out more.
It's unlikely to be measurably more performant in general.
In some cases we might be able to eliminate the initialization checks of a late variable (say when it's initialized in the body of the only generative constructor), but then, we could probably eliminate null checks in the same cases if we wanted to.
lib/src/rules/use_late.dart
Outdated
void visitFieldDeclaration(FieldDeclaration node) { | ||
for (var variable in node.fields.variables) { | ||
if (Identifier.isPrivateName( | ||
(node.parent as ClassOrMixinDeclaration).name.name) || |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe that it's possible to define a static field in an extension, in which case the node's parent will be an ExtensionDeclaration
and this cast will fail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated to handle extensions
lib/src/rules/use_late.dart
Outdated
return; | ||
} | ||
final myVisitor = _MyVisitor(context, variable); | ||
myVisitor.visitCompilationUnit(_compilationUnit); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems very inefficient. Consider writing a single visitor that will collect all of the nodes representing the declaration of a private top-level variable or private field and all of the nodes representing references to private variables or fields, and then compare the two lists after the visitor has returned.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
lib/src/rules/use_late.dart
Outdated
DartTypeUtilities.getCanonicalElementFromIdentifier(node) == | ||
variable.declaredElement) { | ||
var parent = node.parent; | ||
while (parent is ParenthesizedExpression) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider using Expression.unParenthesized
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done but the code isn't really clearer (I don't know why the inference failed to pormote to Expression)
@@ -0,0 +1,49 @@ | |||
// Copyright (c) 2016, the Dart project authors. Please see the AUTHORS file |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
'2016' --> '2020'
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
This overall looks like a reasonable lint to me. I didn't look at the implementation - will if fire on the example below (and if so, is that considered an ok "false positive")?
If you're not ok with the lint firing on the above, then you could also check that there are no nullable assignments to x. cc @natebosch @lrhn @munificent for their thoughts |
@leafpetersen The lint will only apply on private members (or public members inside private class). In your example with |
Interesting! It would work for any private non-local variable. However, I think a couple of things would have to be a bit more strict: The privacy constraint would be needed in order to ensure that a scan of a known amount of source code is guaranteed to come up with all accesses. But then it isn't safe to include public instance members inside a private class, because they could be accessed dynamically, and if that class has a public subclass then they can even be accessed statically. But if every write to the variable has a non-nullable type, and every read of the variable is guarded by a throwing null check ( So this should be sound, and it's a really nice property that the transformation reveals a stronger discipline in the use of that variable: Before the transformation it could be null at any time, as far as a reader of the code can see, but after the transformation it is known that the variable will start off as uninitialized, and when it has been initialized it will never go back. We don't get any extra static guarantees about that initialization, but that's the same, not worse. The performance should be the same (being uninitialized can be represented by having the value null, for a late variable with a non-nullable type). So it looks like it would be an improvement whenever it's applicable. |
lib/src/rules/use_late.dart
Outdated
|
||
const _details = r''' | ||
|
||
Use late for private members with non-nullable type to avoid null checks. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Based on Erik's comment, it sounds like code should have about the same performance either way. It would be good to clarify in the details that the purpose is to make it clear in the code that a field / variable is not expected to be null
. Users who are new to null safety might not yet understand the value of late
.
lib/src/rules/use_late.dart
Outdated
class UseLate extends LintRule implements NodeLintRule { | ||
UseLate() | ||
: super( | ||
name: 'use_late', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Given that this only applies to private fields or top-level variables, we should make that explicit in the name so that when/if we decide to add a similar lint for local variables they are clearly differentiated (use_late_for_private_fields_and_variables
? Hopefully you can come up with something better).
lib/src/rules/use_late.dart
Outdated
} | ||
} | ||
|
||
bool _isPrivateNamedCompilationUnitMember(AstNode parent) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Per Erik's comments, if we want to support public fields in private classes, we need to also ensure that there are no instances of either the enclosing class or any subclass of the enclosing class that are ever accessible outside this library.
I suspect that it isn't worth the effort to include these cases, at least at this point, but either way it would be worth capturing this idea and the requirements for doing so in a comment somewhere in this file.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment added
lib/src/rules/use_late.dart
Outdated
@override | ||
void visitCompilationUnit(CompilationUnit node) { | ||
if (node.featureSet.isEnabled(Feature.non_nullable)) { | ||
super.visitCompilationUnit(node); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This only works for libraries without parts. To fix this, it needs to visit all of the compilation units in the library, but it needs to gather "lateable" variables only while visiting the current compilation unit but gather "nullableAccess"es in all of the units.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
lib/src/rules/use_late.dart
Outdated
final LinterContext context; | ||
|
||
final lateables = <VariableDeclaration>[]; | ||
final nullableAccess = <AstNode, Element>{}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I saw this earlier, but forgot to comment on it.
We don't really need to keep track of the element accessed at a given node, just just need to know, for each element, whether we have found a nullable access. Consider using the following instead:
final nullableAccess = <Element>{};
and just adding elements for which a nullable access was found to the set. Checking whether an element is "lateable" is then just a question of checking that it isn't in the set. That should keep this collection much smaller which will also improve performance.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I keep the node to check the offset and evict every node at the same offset as the variable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The other (and in my opinion better) option would be to not add the elements associated with simple identifiers that are inDeclarationContext()
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done
This makes intuitive sense to me. It would be nice to run this across already migrated packages and see if it catches anything. I don't have a sense for whether this is something that folks are likely to need help with. @a14n - do you have real world motivating examples for this from the flutter use case? Something where you would have done it a different way and it was found in code review that |
I'd like to run this lint on the flutter parts I already migrated to see where this lint triggers but it looks like there's a problem somewhere and the following command doesn't work correctly:
It looks like the issue is caused by
When I manually migrated some files I almost always used a nullable type on every field because it's hard to audit every usage for each field (but now you need to put |
I would +1 this. It takes a very careful reading to notice that all uses are explicitly null checked. During the migration, I was being very careful to try to do this manual audit, and did find some cases (and I think found some in code reviews as well - I think maybe in the dart:ui migration?), but in general it's easy to miss. I wonder whether it would be useful to make this apply to local variables as well? |
Should we be looking at a change in the migration tool to prefer Do we expect this lint to primarily have value during the migration, or do we expect it will have value in the long term? |
It feels useful to me long term, especially if it applies to locals. If you're editing some code and eliminate the last unchecked use of a nullable variable (or last assignment of a nullable value to a nullable variable) then this seems like a good change to consider. This also makes me wonder whether there should be a parallel lint that find unnecessary uses of late ? That is, variables which are marked late, but which could be made just regular variables using definite assignment? |
That's a good question. I don't know how often users will run into that situation, but I suspect that it will be fairly difficult to detect without some level of automation, so I'd be inclined to say yes. Is there a performance penalty related to marking a local variable as If nothing else, the code would be cleaner with extra |
I'm very hesitant to speculate about the performance here. Absent optimization, a non-nullable variable marked late will have a performance penalty relative to a definitely assigned non-nullable variable not marked late. Optimization may (or may not) eliminate this penalty. It may not do so now, but may later once optimizations are more mature. Conversely, in principle late + non-nullable is more easily optimizable than non-late + nullable with explicit null checks. But post optimization they may (or may not) be the same.
Yes. I'd argue it's also more robust. With |
super.visitCompilationUnit(node); | ||
|
||
final units = node.declaredElement.library.units; | ||
if (units.every(lateables.keys.contains)) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not positive that there's an issue here, but it might be the case that generated files won't be visited, which would mean that libraries with generated parts wouldn't ever satisfy this condition. (And if it's the case today that all parts are always visited, there's no reason to think that they always will be.)
I guess what I was thinking about was more along the lines of
if (node is a library and nnbd is enabled for the library) {
for (each unit in `LinterContext.allUnits`)
visit the unit to gather lateables (in per unit caches) and nullableAccesses (in a single collection)
for (each unit in `LinterContext.allUnits`)
create lints for any lateable that isn't referenced in a nullable access
for (final unit in units) { | ||
for (var variable in lateables[unit]) { | ||
if (!allNullableAccess.contains(variable.declaredElement)) { | ||
rule.reportLint(variable); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe that this will report the lint for variables declared in one file at the offset of the variable, but in the file currently being visited, which might be a different file. As far as I know, the linter doesn't have good support for reporting lints across a whole library.
What you'll probably need to do is use reporter.reportError
and pass in an AnalysisError
that is created here (rather than use reportLint
which creates the AnalysisError
for you).
We probably need some tests of libraries with parts to ensure that this is working as intended.
// Ideally we need to also ensure that there are no instances of either the | ||
// enclosing class or any subclass of the enclosing class that are ever | ||
// accessible outside this library. | ||
bool _isPrivateNamedCompilationUnitMember(AstNode parent) => |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I didn't communicate clearly. Sorry about that.
What I was trying to say is that Erik pointed out a source of false positives that I suspect will result in enough false positives that we need to get rid of them. I think there are two ways to remove the false positives. One is by doing the work to prove that no instance of the class can escape the library, but that this approach is probably not worth the effort. The other is by not attempting to lint publicly named members of private classes.
The suggestion to add a comment was intended to capture the fact that if we did the extra analysis we could extend the lint to also look for publicly named members of private classes.
Of course, I might be wrong about the false positives, but I don't know whether we have enough ported code to really gather data in this case.
@pq any advice about how to run my PR on flutter and workaround the issue mentioned in an above comment ? |
The Alternatively, if you're setup to build the Dart SDK, you could update As for a workaround, which issue specifically? |
Sorry to not mentioned the real issue, running |
I didn't manage to run the
or with a custom sdk passed to
It's hard to make a lint without being able to test it on real code. :-( |
/ping |
@scheglov: any thoughts on the |
OK, I can reproduce it by running Actually, we have a bug earlier, and we have a guard, but I guess we were shy and did not enable it always, only as an assertion. So, we surprisingly fail later. I'm going to make it a hard exception, and fix As a workaround pass the absolute path for |
@scheglov Thanks for the workaround, it works great and I can now test this lint on flutter! |
After applying the lint on flutter migrated code it works quite well and has detected several places where |
} | ||
|
||
@override | ||
void visitFieldDeclaration(FieldDeclaration node) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe that this needs to invoke super.visitFieldDeclaration(node)
so that the initializer will be visited because late instance fields can reference other instance members.
} | ||
|
||
@override | ||
void visitTopLevelVariableDeclaration(TopLevelVariableDeclaration node) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe that this needs to invoke super.visitTopLevelVariableDeclaration(node)
for a similar reason.
For the record, I opened #2205 to capture Leaf's lint idea. |
It would be good to have tests for those cases at some point, but I'm OK with having this land so that you can start using it. |
See dart-archive/linter#2189 (comment) Change-Id: I6becb616dd8bcb0c9945ea5ad5ac6119bb93e330 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/156441 Reviewed-by: Brian Wilkerson <[email protected]> Commit-Queue: Konstantin Shcheglov <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Assuming Brian's comments about super calls are addressed, LGTM!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎉
Thanks everyone for all the thoughtful back and forth!
* add use_late lint * address review comments * address review comments * handle parts * rename to use_late_for_private_fields_and_variables * address review comments * address review comments * address review comments
Description
Private members with nullable type can be replaced by non-nullable type and
late
if all the access are nullsafe.BAD:
GOOD: