Skip to content

Conversation

@jacob314
Copy link
Contributor

I'm still not done but this is a good sampling to get feedback.

Fyi @pq , @incendial.

Some of the dead code caught was pretty amazing. Also helped clean up a bunch of really dumb async code.

#    - avoid-global-state   <- probably could be enabled as a follow up although most of our use cases are reasonable for just setting bool flags.
#    - avoid-ignoring-return-values. <-- too many false positives. wish this one could be enabled.
#    - avoid-late-keyword <-- generally we think late is ok or at least unavoidable.
#    - avoid-non-null-assertion <-- unrealistically strict for devtools.
#    - avoid-passing-async-when-sync-expected TODO(jacobr): consider re-enabliing.
# This one seems interesting but has too many false positives. Gave it a try including kitchen sink adding parameters to ignore. Generally it didn't seem to catch that many real issues but had tons of false positives.
# - no-equal-arguments:
#    ignored-parameters:
#      - height
#      - width
#      - double-literal-format
#      - defaultSortColumn
#      - left
#      - right
#      - top
#      - bottom
#      - bottomLeft
#      - topLeft
#      - enabledBorder
#   - no-magic-number
# - prefer-match-file-name
# - prefer-static-class
# - avoid-returning-widgets  This one is nice but has a lot of false positives. Would be great to figure out how to make it more tolerant of reasonable patterns where a separate widget would just make things ugly.
# - prefer-single-widget-per-file. <-- just didn't match our style.


_isDarkThemeEnabled = preferences.darkModeTheme.value;
preferences.darkModeTheme.addListener(() {
addAutoDisposeListener(preferences.darkModeTheme, () {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fyi @polina-c this caught some cases where were adding listeners but never removing.


Widget _buildTabbedPage(
BuildContext context,
BuildContext _,
Copy link
Contributor Author

Choose a reason for hiding this comment

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

the lint about unused parameters was great although I wish it was a bit smarter about methods that were torn off in the same file.
I also wish it handled __ as well as _.

Copy link
Contributor

Choose a reason for hiding this comment

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

Could you give more details, how smarter you want it to be?

I also wish it handled __ as well as _.

Looks like a bug to me, since the rule should ignore any amount of _. Could you share an example?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

_buildTabbedPage is private and the only use is within this file is where it torn off. Glad to hear any number of _ should be allowed.

Copy link
Contributor

Choose a reason for hiding this comment

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

Do I understand correctly: you mean that for private methods used as tear off the rule should not check for unused parameters?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

correct as the unused parameters were only there to match the signature required to use it as a tearoff. Its similar to why you shouldn't complain about unused parameters for an @OverRide

Copy link
Contributor

@incendial incendial Dec 21, 2022

Choose a reason for hiding this comment

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

Thank you! Added to backlog, will ship in the next release

ideTheme: ideTheme,
child: _providedControllers(
offline: true,
child: MultiProvider(
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@kenzieschmoll not sure if this is clearer.
Technically this doesn't solve the lint as the lint still gets mad about methods returning List but I think it solves the spirit and in some ways is a little clearer than the opaque helper.

Copy link
Member

Choose a reason for hiding this comment

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

The only downside of pulling the MultiProvider out of the helper is that this creates some duplicated code where we have to manually specify MultiProvider wherever _providedControllers is used, but I do see how _providedControllers already sounds like it should return a List as is.

Why did child: _providedControllers trigger a lint to begin with?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The lint is one to guide you towards creating widgets rather than creating functions that return widgets. It catches a lot of ugly code (particularly cases where the BuildContext was passed around) as well as some false positives like this.

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 wonder if the duplicate code is a little more idiomatic in that it is clear what we are doing is injecting providers.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@incendial this was the case where I couldn't keep the code idiomatic and avoid the lint about functions returning widgets. Seems odd the lint ever fires for functions returning Lists of widgets. Notice that the lint still fires with my fix here as it is upset about the method called that returns a List of all providers for the class.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see, never seen this case before, tbh 😅. If I change the rule to ignore Providers, will this be enough? The more I look at this case, the more I don't understand how a possible "right way" without a method could look like.

Seems odd the lint ever fires for functions returning Lists of widgets

Very common case is when people create a private method returning List and then spread it in the widget's Column or Row.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

So far the only case that is really blocking me is the provider case. The Column and Row cases are interesting. I do buy that those cases are probably usually bugs.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thank you! Added to backlog, will ship in the next release

message: 'Settings',
child: InkWell(
onTap: () async {
onTap: () {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

here and elsewhere the lints caught a ton of cases where we were pretending we were doing useful async things when we weren't. Fyi @pq

Copy link
Contributor Author

@jacob314 jacob314 Dec 15, 2022

Choose a reason for hiding this comment

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

@kenzieschmoll let me know if these cases where we are doing async stuff in tap handlers is useful or noise.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This one seems interesting but has too many false positives. Gave it a try including kitchen sink adding parameters to ignore. Generally it didn't seem to catch that many real issues but had tons of false positives. # - no-equal-arguments:

Do you mean that the amount of false positives was too much to even list in the rule config?
Yeah I was tossing tons of names in the false positives list in the rules config but not seeing any cases where the lint was adding value so I gave up.

avoid-returning-widgets This one is nice but has a lot of false positives. Would be great to figure out how to make it more tolerant of reasonable patterns where a separate widget would just make things ugly.

That's interesting. If I remember correctly, we cleaned it up from all the known false positives we had. Could you share the cases you consider false positive for this rule so I can handle them properly?
I tagged you in the comment where I tried to make this work idiomatically for a case where we needed a helper method that computed the list of all Providers to inject with a MultiProvider.

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah I was tossing tons of names in the false positives list in the rules config but not seeing any cases where the lint was adding value so I gave up.

This rule initially was created for models mapping, when you can have like 2 String fields and then accidentally pass same value to both fields. Maybe it should be restricted somehow to not trigger on widgets, but I'm not sure.

I tagged you in the comment where I tried to make this work idiomatically for a case where we needed a helper method that computed the list of all Providers to inject with a MultiProvider.

Thank you, answered above.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe it should only apply to certain types of parameters. Seems most relevant for string fields where there is more of a sign of duplicate logic than for num fields or arbitrary class fields.

Copy link
Contributor

Choose a reason for hiding this comment

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

Good point, I'll take some time to think

/// Conditional screens can be added to this list, and they will automatically
/// be shown or hidden based on the [Screen.conditionalLibrary] provided.
List<DevToolsScreen> get defaultScreens {
final inspectorTreeController = InspectorTreeController();
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@kenzieschmoll this is an attempt to placate the duplicate args lint which I gave up on. This one should probably be reverted.

Copy link
Member

Choose a reason for hiding this comment

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

what does the duplicate args lint prevent? two arguments aren't allowed to have the same value?

agreed this should be reverted as this is more verbose without an obvious benefit

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah I don't think it is useful. I do think code is generally a bit harder to read when there are multiple args passed in with the exact same expression particularly if the args aren't named but generally I think it has far too many false positives.
Reverted.

import '../shared/survey.dart';

typedef ErrorReporter = void Function(String title, dynamic error);
typedef ErrorReporter = void Function(String title, Object error);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

lints were good at catching a ton of uses of dynamic that were left in our code.

const SizedBox(width: defaultSpacing),
ElevatedButton(
onPressed: actionInProgress ? null : _connect,
onPressed: actionInProgress ? null : () => unawaited(_connect()),
Copy link
Contributor Author

Choose a reason for hiding this comment

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

this is one of the more unfortunate cases for the lints. They make you call out the cases where you are firing and forgetting async methods. On the other hand, that does make the code look a bit ugly.

Copy link
Contributor

Choose a reason for hiding this comment

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

Something about this makes me wonder if onPressed should be accepting an async Callback and that the Button should be accepting the responsibility of unawaiting it.

I feel like it would be prettier if we could do the following instead:
` onPressed: actionInProgress ? null : () async =>_connect()

Copy link
Contributor Author

Choose a reason for hiding this comment

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

It would be interesting if there was a version of the button class that played nicely with async callbacks. For example, the ideal version wouldn't unawait the future (which just drops it on the ground) and would instead make the button show as disabled until the onPressed callback completes. That would probably be generally useful in DevTools. We have hacked around it with custom logic on some pages but a general solution might be feasible.


@override
Widget build(BuildContext context) {
final textTheme = Theme.of(context).textTheme;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@kenzieschmoll I love this lint warning about duplicate code. It false positives a little bit with enum entries and methods with side effects but in general is tremendously useful in helping people optimize duplicate lookups in build methods.

Copy link
Contributor

Choose a reason for hiding this comment

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

It false positives a little bit with enum entries

Could you share an example? I'd like to fix that. Not sure that the problem with side effects is fixable, probably a candidate for // ignore.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The lint fires for no good reason if you import a library with a named prefix.
For example:

import 'library_with_enum' as libraryName
void main() {
  print(libraryName.EnumName.entry1);
  print(libraryName.EnumName.entry2);

}

Copy link
Contributor

Choose a reason for hiding this comment

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

Thank you, I've added to backlog!

}
}

class _NotificationOverlay extends StatelessWidget {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

@kenzieschmoll I moved some but not all cases where we had functions that should have been widgets over to using widgets.

@incendial
Copy link
Contributor

@jacob314 thank you for mentioning!

avoid-ignoring-return-values. <-- too many false positives. wish this one could be enabled.

Yeah, I like the idea behind it too, but unfortunately, it's really hard to distinguish if a value supposed to be used or was left unused by mistake. Right now this is more like a "run from time to time" type of a rule. Sorry for that.

This one seems interesting but has too many false positives. Gave it a try including kitchen sink adding parameters to ignore. Generally it didn't seem to catch that many real issues but had tons of false positives. # - no-equal-arguments:

Do you mean that the amount of false positives was too much to even list in the rule config?

avoid-returning-widgets This one is nice but has a lot of false positives. Would be great to figure out how to make it more tolerant of reasonable patterns where a separate widget would just make things ugly.

That's interesting. If I remember correctly, we cleaned it up from all the known false positives we had. Could you share the cases you consider false positive for this rule so I can handle them properly?


Widget _buildConnectInput() {
return Column(
final connectorInput = Column(
Copy link
Member

Choose a reason for hiding this comment

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

should we make this a private helper widget instead?

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 this pattern is better for cases where you are just trying to avoid a very deep and hard to read build method. The # of params this widget would need relative to its impl would be silly and placing the code inline avoids the illusion that Flutter does anything smart optimizing it.

// Make a copy so we do not remove a notification from [_notifications]
// while iterating over it.
final notifications = List<_Notification>.from(_notifications);
final notifications = List<_Notification>.of(_notifications);
Copy link
Member

Choose a reason for hiding this comment

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

was this caught by a lint? what is the lint preventing?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is a lint suggested at https://github.com/dart-lang/linter/issues/2555
that Dart Code Metrics implemented.

Comment on lines 129 to 130
final Comparable valueA = getValue(a) as Comparable;
final Comparable valueB = getValue(b) as Comparable;
Copy link
Member

Choose a reason for hiding this comment

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

are these casts still necessary after changing the return type of getValue?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Unfortunately yes. They were hidden by using dynamic.


void _updateAfterIsolateReload(
Event reloadEvent,
Event _,
Copy link
Member

Choose a reason for hiding this comment

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

probably don't need this trailing comma anymore since this should all fit on one line

Comment on lines 1500 to 1501
unawaited(() async {
Clipboard.setData(
Copy link
Member

Choose a reason for hiding this comment

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

this is confusing... why does Clipboard.setData need to be wrapped as an unawaited async callback?

// a system isolate.
onPressed: ((isPaused && !resuming) && !isSystemIsolate)
? controller.resume
? () => unawaited(controller.resume())
Copy link
Member

Choose a reason for hiding this comment

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

this is a bit uglier to have to do this everywhere


addAutoDisposeListener(widget.controller.isPaused, _updateStatus);
addAutoDisposeListener(
widget.controller.isPaused, () => unawaited(_updateStatus()));
Copy link
Member

Choose a reason for hiding this comment

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

nit: trailing comma

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done. All regular lints are now resolved here and elsewhere.

// todo: should we check that widget.controller != oldWidget.controller?
addAutoDisposeListener(widget.controller.isPaused, _updateStatus);
addAutoDisposeListener(
widget.controller.isPaused, () => unawaited(_updateStatus()));
Copy link
Member

Choose a reason for hiding this comment

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

here too. our lints should catch this

Copy link
Contributor Author

Choose a reason for hiding this comment

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

done here and elsewhere.

displayProvider(context, variable, onPressed, debuggerController),
onItemSelected: (variable) async =>
onItemPressed(variable, debuggerController),
onItemSelected: (variable) async => onItemPressed(variable),
Copy link
Member

Choose a reason for hiding this comment

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

does onItemPressed need to be awaited or unawaited?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fyi @incendial. Wonder why the lints for unneeded async didn't trigger here.
This code could be further simplified to just
onItemSelected: onItemPressed

Copy link
Contributor

Choose a reason for hiding this comment

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

Agree, noted

);
await widget.controller.collapseDetailsToSelected();
}),
ga.select(
Copy link
Member

Choose a reason for hiding this comment

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

why do we no longer want to blockWhileInProgress?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@incendial this is an example of a good success story for the Dart Code Metrics lints. The lints caught that ga.select never needed to be async which triggered further lints to clarify that we never needed to be calling blockWhileInProgress as nothing async was actually happening in the whole block.

Comment on lines +29 to +30
height: 6,
width: 20,
Copy link
Member

Choose a reason for hiding this comment

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

not your fault but these should be named consts

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed. FYI there is a Dart Code Metrics lint for exactly that problem we can enable next. Leaving moving these const off this CL as it is already too big and too error prone.

}

Widget _buildHttpTimeGraph(BuildContext context) {
Widget _buildHttpTimeGraph() {
Copy link
Member

Choose a reason for hiding this comment

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

we should move these to private helper widgets instead of helper methods

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Leaving that for a follow-up enabling the DCM lint to use private helper widgets after enabling the associated lint.

Comment on lines +72 to +80
@override
Future<void> init() {
return Future.value();
}

@override
Future<void> onBecomingActive() {
return Future.value();
}
Copy link
Member

Choose a reason for hiding this comment

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

what lint is forcing these overrides?

Copy link
Member

Choose a reason for hiding this comment

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

if PerformanceFeatureController returns these values by default, do the subclasses need these empty overrides?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was a judgment call. The base class had 3 methods tagged as async that weren't really async. 1 of the 3 methods was actually overridden everywhere so could be made abstract. These other two were overridden almost everywhere except for this case where a no-op implementation had to be added. In general, if a member should almost always be overridden I think it is cleaner to make it abstract. In addition, overriding a method returning future is very ugly as you end up awaiting the super version of the method which is quite silly given the super version was. a no-op.

..height = '100%'
..width = '100%';

// TODO(jacobr): why does this have to be ignored?
Copy link
Member

Choose a reason for hiding this comment

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

I'm not sure on the details of why, but this is a weird flutter web thing. @ditman do you know the reasoning for this?

Copy link
Member

Choose a reason for hiding this comment

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

This needs to be ignored because the API surface of dart:ui for the web is bigger than for mobile (it has the webOnlyXXX methods that are not defined in the mobile version, for example, or the platformViewRegistry object).

analyzer flags this as you calling "things that don't exist" in the API because it only sees the proper dart:ui mobile package, not the web re-implementation with extra methods.

A workaround to this ignore is to apply a shim for the web version of dart:ui (but I've only used it in web-only packages), like this one, used here.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks. Referenced the issue.

throw Exception('Error loading data with tag "$tag": ${e.toString()}');
Error.throwWithStackTrace(
Exception('Error loading data with tag "$tag": ${e.toString()}'),
stackTrace);
Copy link
Member

Choose a reason for hiding this comment

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

nit: tc

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. Interestingly this is a case the DCM trailing comma lint will flag. As a follow up I may try enabling it.

Comment on lines 493 to 501
data = tag == groupByUserTag || tag == groupByVmTag
? CpuProfilePair.withTagRoots(
fullData,
tag == groupByUserTag
? CpuProfilerTagType.user
: CpuProfilerTagType.vm,
)
: CpuProfilePair.fromUserTag(fullData, tag);
Copy link
Member

Choose a reason for hiding this comment

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

this double nest is harder to read imo

Copy link
Contributor Author

Choose a reason for hiding this comment

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

reverted

} else {
return treemapIncreaseColor;
}
return byteSize < 0 ? treemapDecreaseColor : treemapIncreaseColor;
Copy link
Member

Choose a reason for hiding this comment

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

did you merge with master? some of these changes are looking familiar from your last PR

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed.


@JS('gtag')
external void _gTagCommandName(String command, String name, [dynamic params]);
external void _gTagCommandName(String command, String name, [Object params]);
Copy link
Member

Choose a reason for hiding this comment

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

should this be Object? - here and below

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done. Here and elsewhere made a lot more cases of dynamic into Object? instead of Object

} else {
packagesPath = io.Directory.current.parent.path;
}
packagesPath = io.Directory.current.path.endsWith('test')
Copy link
Member

Choose a reason for hiding this comment

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

I wonder why the duplicated code lint isn't catching that io.Directory.current and io.Directory.current.parent are duplicated

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 tuned it up to 3 dupes to reduce the # of false positives. Can revisit with 2 dupes after the initial cleanup lands.

@jacob314 jacob314 force-pushed the code_metric_fixes2 branch 4 times, most recently from 4c73118 to a0e30c1 Compare December 20, 2022 22:42
Copy link
Contributor

@CoderDake CoderDake left a comment

Choose a reason for hiding this comment

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

Took a look at part of this, only 1 comment so far

const SizedBox(width: defaultSpacing),
ElevatedButton(
onPressed: actionInProgress ? null : _connect,
onPressed: actionInProgress ? null : () => unawaited(_connect()),
Copy link
Contributor

Choose a reason for hiding this comment

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

Something about this makes me wonder if onPressed should be accepting an async Callback and that the Button should be accepting the responsibility of unawaiting it.

I feel like it would be prettier if we could do the following instead:
` onPressed: actionInProgress ? null : () async =>_connect()

@jacob314 jacob314 force-pushed the code_metric_fixes2 branch 6 times, most recently from 047f630 to 490231d Compare December 21, 2022 22:23
@jacob314
Copy link
Contributor Author

All comments addressed and build is now green. PTAL.

@jacob314 jacob314 merged commit 44a57db into flutter:master Dec 21, 2022
@jacob314 jacob314 deleted the code_metric_fixes2 branch December 21, 2022 23:59
@CoderDake CoderDake mentioned this pull request Jan 24, 2023
7 tasks
@DartDevtoolWorkflowBot DartDevtoolWorkflowBot mentioned this pull request Jan 24, 2023
7 tasks
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.

6 participants