Dart type checking with generics - dart

I have a class:
class StreamWithValue<T> extends Stream<T> {
T value;
...
}
and now:
Stream<bool> _stream;
StreamWithValue<bool> get stream=> _stream;
This compiles without errors even though its incorrect since obviously _stream isn't of type StreamWithValue. Is there a way for a more strict type checks? I am using Dart 2.1.2, Android Studio 3.3.2. Strangely the compiler correctly finds an error here:
StreamWithValue get stream2 => _stream;

By default, Dart 2 allows implicit downcasts to derived types. To disallow them, in your analysis_options.yaml file (creating it if necessary), set:
analyzer:
strong-mode:
implicit-casts: false
Doing so will generate an analysis error:
The return type 'Stream' isn't a 'StreamWithValue', as defined by the method 'stream'.
You might also be interested in setting:
analyzer:
strong-mode:
implicit-dynamic: false
For more information, see https://www.dartlang.org/guides/language/analysis-options.

Related

What is the ambiguate function doing?

This is a function in the main.dart file of the just_audio example. I don't understand what's going on with the "ambiguate" line. I understand the bang operator in this context casts to the "underlying type" but in this case there is no underlying type, I don't think. The underlying type is <T?>. I'm only familiar with what that means when I see it in documentation as "a type goes here." If it's in actual code, not sure what it's doing.
void initState() {
super.initState();
ambiguate(WidgetsBinding.instance)!.addObserver(this);
SystemChrome.setSystemUIOverlayStyle(const SystemUiOverlayStyle(
statusBarColor: Colors.black,
));
_init();
}
The ambiguate function from common.dart in the same lib folder:
T? ambiguate<T>(T? value) => value;
https://github.com/ryanheise/just_audio/tree/minor/just_audio/example/lib
The ambiguate function casts a value to its nullable type, so an int is cast to int?.
That allows using the ! (null assert) operator on the value without a warning, because a value of type int cannot be null, so you don't need to assert that it isn't.
The reason for doing so is that in a program which also contains non-null-safe code, the value can actually be null at runtime.
Or because you don't actually know whether the type will be int or int?, because it changed between versions of a library, and you don't want to lock yourself to only the newest version.
Which means that the only reason to use the function is that you expect your code to run against two different and incompatible versions of the same library, one null-safe or non-null-safe where a function can return null, and a newer null-safe version where the function cannot return null and is typed as such.
Then you can do ambiguate(uncertainValue)?.doSomething(), and it does something if the value isn't null, and it compiles against both versions without warning.
If you are not trying to make your code work against two different versions of the same library, which differ in nullability-behavior, then don't use ambiguate.
Even then, consider whether it'd just be easier to require the new version of the library, and lean into null safety.
(This particular use seems unnecessary. Doing ambiguate(something)!.method() will throw an error if the value is null, but so will something.method(), which will also not give any warnings. Well, unless the other version of the library is null safe and returns a nullable value, but then you shouldn't be using ! on it.)

Nullability mismatch in simple assignment after switching to sound null safety

I switched to sound null safety and started getting runtime error in a simple assignment, that should never happen with sound null safety:
final widgetOnPressed = widget.onPressed;
Error:
type '(LogData) => void' is not a subtype of type '((LogData?) => void)?'
I can repro it for Flutter versions 2.12.0-4.1.pre and 2.13.0-0.0.pre.505.
PR: https://github.com/flutter/devtools/pull/3971
Failing line: https://github.com/flutter/devtools/blob/9fc560ff2e6749459e2ca6a1dc00bf6fb16ed93b/packages/devtools_app/lib/src/shared/table.dart#L1184
To repro, start DevTools at this PR for macos, connect to an app and click the tab 'Logging'. DevTools will show red screen and error in console.
Is it dart bug or the app bug? If it is the app bug, how can I debug it?
It's a bug in your code.
You didn't say which kind of error you got - a compile-time error or a runtime error. I'm guessing runtime error. (Well, you did say to launch it in the debugger, so that is a good hint too.)
The line final widgetOnPressed = widget.onPressed; looks like it can't possibly fail. After all, the type of the local variable is inferred from the expression assigned to it, and the runtime value of that expression will surely be a subtype of the static type because the type system is sound!
Isn't it? ISN'T IT?
It's not, sorry. Dart 2's type system is mostly sound, even more so with null safety, but class generics is covariant, which can still be unsound. It's fairly hard to hit one of the cases where that unsoundness shows its ugly head, but returning a function where the argument type is the class's type variable is one.
Your state class extends State<TableRow<T?>>, so the widget getter returns a TableRow<T?>. The onPressed of that type has type ItemCallback<T?>?, aka, void Function(T?)?.
You create a _TableRowState<LogData>, with its widget which has static type TableRow<LogData?>, but you somehow manage to pass it a TableRow<LogData> instead. That's fine. Class generics are covariant, so all is apparently fine at compile-time.
Then you do final widgetOnPressed = widget.onPressed;.
The static type of widgetOnPressed is void Function(LogData?) here.
The actual runtime type of onPressed is void Function(LogData) because it's from a TableRow<LogData>.
A void Function(LogData) is-not-a void Function(LogData?) because the former cannot be used in all places where the latter can (in particular, it can't be used in a place where it's called with null).
This assignment is potentially unsound, and actually unsound in this case. The compiler knows this and inserts an extra check to ensure that you don't assign a value to the variable which isn't actually valid. That check triggers and throws the error you see.
How do you avoid that?
Don't create a TableRow<LogData> where a TableRow<LogData?> is required.
Or type the variable as:
final ItemCallback<T>? widgetOnPressed = widget.onPressed;
(no ? on the T).
Or rewrite everything to avoid returning a function with a covariant type parameter (from the class) occurring contra-variantly (as an argument type).
Which solution fits you depends on what you want to be able to do.

Why dart analyzer fails to recognize a type error?

Here's the code:
static const _defaults = <String, dynamic>{
'api_key': '4839234792374',
'enabled': false,
'concurrency': 4,
};
String getString(String key) {
return _remoteConfig == null ?
_defaults.containsKey(key) && _defaults[key] :
_remoteConfig.getString(key);
}
The bug is obvious (and shame on me, was produced by blind copy-paste from similar getBool(key) function. If the _remoteConfig is null, the execution hits the bool && String path and I get the runtime exception type 'String' is not a subtype of type 'bool'. Totally legit, but why does analyzer not see it? The execution flow is crystal clear, one path returns String another path (theoretically) returns dynamic and the return type is String which means that all paths return String.
What I don't understand?
Dart 2.12.0
pedantic 1.11.0
Analyzer options:
include: package:pedantic/analysis_options.yaml
analyzer:
exclude:
- lib/generated/*
- lib/**/*.g.dar
I think you're asking two questions:
Why doesn't the analyzer complain about _defaults.containsKey(key) && _defaults[key]?
Why doesn't the analyzer complain about the ternary expression with different types along its two paths?
For #1: Since _defaults[key] returns type dynamic (which could be a bool at runtime), I wouldn't expect an analysis complaint.
For #2: Since the two paths have different types, the type of the ternary expression is the common base type: Object. If implicit casts are enabled, Object is then automatically cast to the String return type.
The analyzer does catch both errors if you disable implicit casts in your analysis_options.yaml configuration file:
analyzer:
strong-mode:
implicit-casts: false
Running the analyzer then prints:
error • The operands of the operator '&&' must be assignable to 'bool' at ... • (non_bool_operand)
error • A value of type 'Object' can't be returned from the function 'getString' because it has a return type of 'String' at ... • (return_of_invalid_type)
(I'm surprised that implicit-casts: false triggers an analysis error for the && expression with a dynamic operand,; maybe my understanding isn't quite accurate, or maybe that's a bug in the analyzer.)

dart nullsaftey and using old packages

I've enabled the dart 2.8 nullsaftey experiment.
I've converted my app to nullsaftey but its using an old pre-nullsafety package.
The problem is that the old package has a method which can return null:
/// Returns the environment variable with [name] or null if it doesn't
/// exist
String env(String name);
Which is used as follows:
var home = env('HOME');
If the HOME environment variable is missing, env returns null.
The problem is that env is declared as returning a String.
So when I write
var home = env('HOME');
home ??= '/home';
I get an error:
The operand can't be null, so the condition is always false.
Try removing the condition, an enclosing condition, or the whole conditional statement.
Given that all the nullsaftey release announcements say you can use nullsaftey with older packages, I'm guessing there is some way to declare an imported packages as non-nullsafe.
Problem is that I can't find any documentation on how to do this.
null safety has not been released yet! that is why you need to provide the experiment flag.
Language versioning
By default, whether or not null safety is supported in a library is determined by its language version. Any language version 2.8 or lower counts as opted out of null safety, and 2.9 or higher (subject to change) is opted in. The language version itself can come from one of two places:
The minimum bound of the package's declared SDK constraint. The following package will have a language version of 2.8.
name: foo
env:
sdk:
">=2.8.0 <3.0.0"
A language override comment at the top level of the file, before any other declarations. The following library will have a language version of 2.8.
// #dart=2.8
class Foo {}
The language override comment will take precedence over the SDK constraint, but only within the single library where it is declared.
Interaction between null safe and non-null safe code
The problem you are having is reproducible without different packages or incorrect language versions though, and has to do with the interaction between null-safe and non-null-safe code. Consider the following example:
// #dart=2.8
String foo() {
return null;
}
// #dart=2.9
import 'a.dart';
void main() {
var value = foo();
value ??= 'asd';
}
The return type of foo doesn't become String?, instead it gets tagged as String* - this is known as a legacy type. A legacy type is treated as a non-null type in opted in libraries. The goal of legacy types is to make it easier to migrate to null-safety through an in-order migration
Consider the example below:
// #dart=2.9
void foo(String value) {
// do something with non-null String.
}
// #dart=2.8
import 'a.dart';
void main() {
foo(getStringFromAPI());
}
While foo requires a non-null string, it isn't possible for the entry-point to actually pass it one - since it has not opted in yet. Without the treatment of legacy types as non-nullable types, it would not be possible to gradually migrate - because all libraries would need to be updated at once, or only updated to accept nullable types.
Out of order migration
By calling code that has not been migrated to null-safety from a null safe library, you are increasing the risk that you will be broken when that dependency eventually migrates. In you example, if home was treated as non-nullable then updating to a version of the dependency with an updated return value of String? would cause a compilation error.
For your specific case, I would recommend specifically annotating the type of home as String?. This is a perfectly valid type annotation, since in general T and T* are always assignable to T?. It also more correct, since you know the API can return null.
String? home = env('HOME');
home ??= '/home';
EDIT June 2021:
Null safety has released, yay! The first version of Dart with null safety enabled by default ended up being 2.12 and not 2.9 as documented in the question above.

What's the benefit of using as when creating an instance, generally inside a factory constructor

class Product {
final int id;
Product({this.id});
factory Product.fromMap(Map<String, dynamic> map) {
return Product(
id: map['id'] as int, // Why as?
);
}
}
This is a pattern I have seen used by Google, the main question is what's the need of using as there, because below code does the job equally well.
Product(
id: map['id'],
)
Can anyone tell me any advantage of using as in above code?
This comes down to how the analyzer is configured and if you have disabled implicit-casts in analysis_options.yaml:
analyzer:
strong-mode:
implicit-casts: false
The problem in your code is that the type of the map is defined as Map<String, dynamic> so we don't really know the type of the values in the map on compile time. And if we have disabled implicit casting we cannot just assign a dynamic into a int variable without any implicit type casting with as.
It is recommended to use implicit-casts: false (and implicit-dynamic: false) to make the analyzer more strict about your typing which can both make more readable code but only catch errors where you are casting types to other types without your knowing which in the end could have give a runtime error.
There are more about in the documentation:
https://dart.dev/guides/language/analysis-options

Resources