-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Linter needs to warn about single-item records #59311
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
Comments
That should probably be "single positional item records". We also allow I wouldn't remove it from the language, but we could remove it from the grammar, so you have to write We would not allow Should it also affect record types and patterns? Today you can do: class Opt<T> { // extension type, soon.
final (T,)? _opt;
Opt.some(T value) : _opt = (value,);
Opt.none() : _opt = null;
bool get hasValue => _opt != null;
T get value => switch(_opt) {(var v,) => v, null => throw StateError("No value")};
T? get tryValue => _opt?.$1;
} Even if I change We might have to introduce final Record1<T>? _opt;
///...
switch(_opt) {Record1($1: var v) => v, ... Maybe it wouldn't be be too bad if you could generally write That's like the Anyway. that's all unlikely to happen now, so marking as analyzer issue. |
I'll let you handle any deeper language design questions, but at this point the main thing I would need as an end user is simply a linter warning if I try to create a single positional item record. Again the main reason this is necessary is that when working in Dart, you start to train yourself to end basically everything with a comma, to get the formatter to work, and where a comma gives a syntax error (e.g. between A more thoughtful approach (only putting commas in list contexts) is of course the solution, but the point is that muscle memory will usually override thoughtful approaches, and this issue is likely to come up a lot, for a lot of people. |
Thanks for the report and @lrhn for the thoughtful response! We've had conversations about this confusion before and I do think it would be nice to do something to help folks stay on the rails. Ideally we could address this with a better error message but also possibly with a more pedantic "avoid singleton positional records" lint. fyi @dart-lang/language-team @bwilkerson |
Yeah I think changing:
to
or similar, would have the biggest bang for our buck. A rule could be useful as well. |
Along those lines we might also consider using semantic highlighting. LSP doesn't have a "record" highlight type, but it does have a "struct", which might be close enough. If typing the trailing comma changed the color of the formerly parenthesized expression to the color of a record, that might be enough to draw attention to the problem. |
That doesn't cover all situations where the user should be alerted. In my case the return type of a lambda was I suggest at a minimum adding a comma to the display of record types that have just one positional field, since that is the only way to construct them anyway. |
+1 |
We debated single-element (and zero-element) records heavily before deciding to support them. See dart-lang/language#1301 and dart-lang/language#2386. For me, the most important motivation for supporting them is that eventually we'd like to be able to support spreading records into argument lists (dart-lang/language#893). For that to work generally across all argument list shapes, we'll need to support records of a single positional element.
For what it's worth, you can't put a trailing comma in a parenthesized expression (which is why we were able to steal that syntax to mean "single-element record"), index operator, type parameter list, or type argument list.
+1. Putting the trailing comma in the type wouldn't hurt either: "A value of record type '(String?,)' can't be returned". Overall, I think it's worth having single-positional-element records. The next question is whether to have syntax for them. We debated that too. I agree with the issue here that the trailing comma can be a little confusing or error-prone, but it more or less seems to work OK. There are no perfect syntaxes, and it's certainly hard to design elegant ones on top of an existing widely-used language. |
Right, that's the whole reason I filed this bug report :-) The issue is that if you're used to ending almost every line with a comma or semicolon, then it is really easy to accidentally slip one in where it's not supposed to be, and that can wreak havoc at locations far from the erroneous comma. A similar issue (which I reported in another bug) is the erroneous lambda syntax
Right, this is exactly what I proposed in my last comment. I don't see any downside to adding the trailing comma to the It's probably only a 2-line change, too, along with some changes or additions to tests. |
Another high-value improvement might be to change the formatter to always re-format single-value records (
foo,
) to (foo,) The behavior would be consistent with the behavior as if the comma wasn't there, even for multi-line contents. final x = (
max(
1,
2,
),
); final x = (max(
1,
2,
),); |
This seems like a good move, but i in a weird way it doesn't address the cognitive issue here, where the comma got added by force of habit, precisely because people are used to putting a comma at the end of everything, to force an indent/outdent. Seeing one's code suddenly indented by the formatter might make sense on the surface, because your brain already decided that adding a comma was a good idea, and every time you do that, the indenter takes this action. There would be no surprise factor that would alert the user that you just did something erroneous. An editor could add a "ghost label" (not sure what these are called -- text that appears at the end of a line in VS Code) that says |
This is done in 1c4ad79, available on the Flutter beta channel today (works in DartPad). |
@srawlins awesome, thanks. |
The formatter will generally do that, as in the example here. And it's the only place in the formatter where you'll get But if the entire record doesn't fit on one line or the element expression contains a split, it does split the record like it does with multi-element or named-single-element ones: main() {
var single = (
veryLongOperand +
another___________________________________________________,
);
var multiple = (
veryLongOperand +
another___________________________________________________,
veryLongOperand +
another___________________________________________________,
);
var singleNamed = (
name: veryLongOperand +
another___________________________________________________,
);
}
For what it's worth, that latter example looks pretty confusing to me. Instead of the final x = (
max(
1,
2,
),
); The nice thing about that is that the |
For the following code:
The line marked
(2)
has the warningThe operand can't be null, so the condition is always 'false'.
The line marked
(3)
has the warningA value of type '(String?)' can't be returned from the method 'getName' because it has a return type of 'Future<String>'
The type of
name
is(String?)
, which looks deceptively likeString?
, but it is actually a single-item record, because of the stray comma on line(1)
!It took me a while to figure out what was going on here, because I assumed that the type
(String?)
was in fact the same as the typeString?
...It is very easy to make this sort of mistake, because it is customary to put a trailing literally everywhere you can put one in Dart, especially in Flutter code, because then the formatter makes the structure of the code much clearer.
In my opinion, it shouldn't be possible to create single-item records. Actually, Dart wouldn't interpret
(value)
as a record. But apparently it does recognize(value,)
as a record.I think the best fix here would be to ignore the empty field at the end of a record definition that ends in a comma before the closing parenthesis, and proceed as if the comma weren't there. That would make
(value,)
act the same as(value)
. Although the downside of this is that before record syntax existed,(value,)
would not have been valid syntax for a parenthesized expression...Therefore, maybe the best thing to do here is simply give the user a warning if they try to declare a single-field record.
dart info
):The text was updated successfully, but these errors were encountered: