"[SEVERE] type 'YamlList' is not a subtype of type 'List<String>' in type cast" when overriding builder options
I have a builder defined like this:
builders:
my_builder:
import: my_package:my_builder/builder.dart
builder_factories:
- myBuilder
build_extensions:
$package$:
- lib/generated/my_file.dart
auto_apply: dependents
build_to: source
defaults:
options:
dirs:
- dir_1
and then in a different project I use this bilder and overwrite its options to be:
targets:
$default:
builders:
my_package:my_builder:
options:
dirs:
- dir_a
The builder factory is like this:
Builder myuilder(BuilderOptions options) {
return MyBuilder(options.config['dirs']! as List<String>);
}
It works when I don't override the dirs configuration in a target, but when I do I get
SEVERE]
type 'YamlList' is not a subtype of type 'List<String>' in type cast
When I debug the factory it the value really is a YamlList.
Is this expected? Why does it behave differently with and without the options being overridden in a dependent package?
This looks expected to me, we are parsing things as yaml, which allows for values other than strings in lists. It isn't safe to cast this directly to a List<String> here, you need to either .cast<String>() or create a new list (using List.from etc).
I don't know why this would work when you aren't overriding it, unless the builder was not applied at all? I would expect the same thing to happen with the default options.
It was auto applied and I can debug both situation and without the options being overwritten I get a List<String> hence the cast was working; when I overwrite the options I get a YamlList instead. Yeah, I changed the code to be
(options.config['dirs']! as List<dynamic>).cast<String>()
and it works in both cases. But the change in behavior is pretty strange.
I'm very surprised you'd ever get a List<String> where that cast would succeed, can you elaborate on the setup to see that behavior?
Sure.
$ dart --version
Dart SDK version: 2.16.2 (stable) (Tue Mar 22 13:15:13 2022 +0100) on "macos_x64"
Here is the code:
import 'package:build/build.dart';
class MyBuilder implements Builder {
final List<String> dirs;
MyBuilder(this.dirs);
@override
final buildExtensions = const {
r'$package$': ['lib/generated/my_file.dart'],
};
@override
Future<void> build(BuildStep buildStep) async {
print('$dirs');
}
}
import 'package:build/build.dart';
import 'src/my_builder.dart';
Builder myBuilder(BuilderOptions options) {
print(options.config['dirs'].runtimeType); // prints List<String>
return MyBuilder(
options.config['dirs']! as List<String>,
);
}
builders:
my_builder:
import: package:my_package/builder.dart
builder_factories:
- myBuilder
build_extensions:
$package$:
- lib/generated/my_file.dart
auto_apply: root_package
build_to: source
defaults:
options:
dirs:
- dir_a
# targets:
# $default:
# builders:
# my_package:my_builder:
# options:
# dirs:
# - dir_a
name: my_package
version: 1.0.0+1
environment:
sdk: '>=2.16.2 <3.0.0'
dependencies:
build: ^2.3.0
build_config: ^1.0.0
build_runner: ^2.1.0
This works file. Uncomment the targets section in build.yaml and it results in the error mentioned above. I see YamlList in the debugger only when targets is active.
Ah, I think I know what is happening here. The default options end up written to the generated build script as a list literal in Dart, which then gets a type of List<String> from inference. For example:
https://github.com/dart-lang/build/blob/8ebdc5a9d5db8cea9cdf0cb2df80ab25fe8f16db/_test/test/goldens/generated_build_script.dart#L114
In the short term we probably want to explicitly type that as List<dynamic> so that this behavior is less confusing.
In the long term I like a suggestion @jakemac53 made offline - we should stop encoding this data into the Dart code. If we use the version parsed at build time it will be more consistent, and may have better invalidation behavior.