feat(dart/transform): Record property metadata
Update the transformer to generate code registering annotations on class properties, getters, and setters. Closes #1800, #3267, #4003
This commit is contained in:
parent
cc1d758eba
commit
64e8f93f32
|
@ -5,6 +5,7 @@ import 'package:angular2/src/transform/common/annotation_matcher.dart';
|
||||||
import 'package:angular2/src/transform/common/logging.dart';
|
import 'package:angular2/src/transform/common/logging.dart';
|
||||||
import 'package:angular2/src/transform/common/model/reflection_info_model.pb.dart';
|
import 'package:angular2/src/transform/common/model/reflection_info_model.pb.dart';
|
||||||
import 'package:angular2/src/transform/common/names.dart';
|
import 'package:angular2/src/transform/common/names.dart';
|
||||||
|
import 'package:angular2/src/transform/common/property_utils.dart';
|
||||||
import 'package:barback/barback.dart' show AssetId;
|
import 'package:barback/barback.dart' show AssetId;
|
||||||
|
|
||||||
import 'annotation_code.dart';
|
import 'annotation_code.dart';
|
||||||
|
@ -16,20 +17,26 @@ class ReflectionInfoVisitor extends RecursiveAstVisitor<ReflectionInfoModel> {
|
||||||
/// The file we are processing.
|
/// The file we are processing.
|
||||||
final AssetId assetId;
|
final AssetId assetId;
|
||||||
|
|
||||||
final AnnotationVisitor _annotationVisitor;
|
|
||||||
final ParameterVisitor _parameterVisitor = new ParameterVisitor();
|
|
||||||
|
|
||||||
/// Whether an Angular 2 `Reflection` has been found.
|
|
||||||
bool _foundNgReflection = false;
|
|
||||||
|
|
||||||
/// Responsible for testing whether [Annotation]s are those recognized by
|
/// Responsible for testing whether [Annotation]s are those recognized by
|
||||||
/// Angular 2, for example `@Component`.
|
/// Angular 2, for example `@Component`.
|
||||||
final AnnotationMatcher _annotationMatcher;
|
final AnnotationMatcher _annotationMatcher;
|
||||||
|
|
||||||
ReflectionInfoVisitor(AssetId assetId, AnnotationMatcher annotationMatcher)
|
final AnnotationVisitor _annotationVisitor;
|
||||||
: this.assetId = assetId,
|
final ParameterVisitor _parameterVisitor = new ParameterVisitor();
|
||||||
_annotationMatcher = annotationMatcher,
|
final _PropertyMetadataVisitor _propMetadataVisitor;
|
||||||
_annotationVisitor = new AnnotationVisitor(assetId, annotationMatcher);
|
|
||||||
|
/// Whether an Angular 2 `Reflection` has been found.
|
||||||
|
bool _foundNgReflection = false;
|
||||||
|
|
||||||
|
ReflectionInfoVisitor._(this.assetId, this._annotationMatcher,
|
||||||
|
this._annotationVisitor, this._propMetadataVisitor);
|
||||||
|
|
||||||
|
factory ReflectionInfoVisitor(
|
||||||
|
AssetId assetId, AnnotationMatcher annotationMatcher) {
|
||||||
|
var annotationVisitor = new AnnotationVisitor(assetId, annotationMatcher);
|
||||||
|
return new ReflectionInfoVisitor._(assetId, annotationMatcher,
|
||||||
|
annotationVisitor, new _PropertyMetadataVisitor(annotationVisitor));
|
||||||
|
}
|
||||||
|
|
||||||
bool get shouldCreateNgDeps => _foundNgReflection;
|
bool get shouldCreateNgDeps => _foundNgReflection;
|
||||||
|
|
||||||
|
@ -92,9 +99,44 @@ class ReflectionInfoVisitor extends RecursiveAstVisitor<ReflectionInfoModel> {
|
||||||
model.interfaces.addAll(node.implementsClause.interfaces
|
model.interfaces.addAll(node.implementsClause.interfaces
|
||||||
.map((interface) => '${interface.name}'));
|
.map((interface) => '${interface.name}'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Record annotations attached to properties.
|
||||||
|
for (var member in node.members) {
|
||||||
|
var propMetaList = member.accept(_propMetadataVisitor);
|
||||||
|
if (propMetaList != null) {
|
||||||
|
model.propertyMetadata.addAll(propMetaList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_coalesce(model.propertyMetadata);
|
||||||
|
|
||||||
return model;
|
return model;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If a class has a getter & a setter with the same name and each has
|
||||||
|
// individual metadata, collapse to a single entry.
|
||||||
|
void _coalesce(List<PropertyMetadataModel> propertyMetadata) {
|
||||||
|
if (propertyMetadata.isEmpty) return;
|
||||||
|
|
||||||
|
var firstSeenIdxMap = <String, int>{};
|
||||||
|
firstSeenIdxMap[propertyMetadata[0].name] = 0;
|
||||||
|
var i = 1;
|
||||||
|
while (i < propertyMetadata.length) {
|
||||||
|
var propName = propertyMetadata[i].name;
|
||||||
|
if (firstSeenIdxMap.containsKey(propName)) {
|
||||||
|
var propNameIdx = firstSeenIdxMap[propName];
|
||||||
|
// We have seen this name before, combine the metadata lists.
|
||||||
|
propertyMetadata[propNameIdx]
|
||||||
|
.annotations
|
||||||
|
.addAll(propertyMetadata[i].annotations);
|
||||||
|
// Remove the higher index, okay since we directly check `length` above.
|
||||||
|
propertyMetadata.removeAt(i);
|
||||||
|
} else {
|
||||||
|
firstSeenIdxMap[propName] = i;
|
||||||
|
++i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
ReflectionInfoModel visitFunctionDeclaration(FunctionDeclaration node) {
|
ReflectionInfoModel visitFunctionDeclaration(FunctionDeclaration node) {
|
||||||
if (!node.metadata
|
if (!node.metadata
|
||||||
|
@ -126,6 +168,53 @@ class ReflectionInfoVisitor extends RecursiveAstVisitor<ReflectionInfoModel> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Visitor responsible for parsing [ClassMember]s into
|
||||||
|
/// [PropertyMetadataModel]s.
|
||||||
|
class _PropertyMetadataVisitor
|
||||||
|
extends SimpleAstVisitor<List<PropertyMetadataModel>> {
|
||||||
|
final AnnotationVisitor _annotationVisitor;
|
||||||
|
|
||||||
|
_PropertyMetadataVisitor(this._annotationVisitor);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<PropertyMetadataModel> visitFieldDeclaration(FieldDeclaration node) {
|
||||||
|
var retVal = null;
|
||||||
|
for (var variable in node.fields.variables) {
|
||||||
|
var propModel = new PropertyMetadataModel()..name = '${variable.name}';
|
||||||
|
for (var meta in node.metadata) {
|
||||||
|
var annotationModel = meta.accept(_annotationVisitor);
|
||||||
|
if (annotationModel != null) {
|
||||||
|
propModel.annotations.add(annotationModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (propModel.annotations.isNotEmpty) {
|
||||||
|
if (retVal == null) {
|
||||||
|
retVal = <PropertyMetadataModel>[];
|
||||||
|
}
|
||||||
|
retVal.add(propModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<PropertyMetadataModel> visitMethodDeclaration(MethodDeclaration node) {
|
||||||
|
if (node.isGetter || node.isSetter) {
|
||||||
|
var propModel = new PropertyMetadataModel()..name = '${node.name}';
|
||||||
|
for (var meta in node.metadata) {
|
||||||
|
var annotationModel = meta.accept(_annotationVisitor);
|
||||||
|
if (annotationModel != null) {
|
||||||
|
propModel.annotations.add(annotationModel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (propModel.annotations.isNotEmpty) {
|
||||||
|
return <PropertyMetadataModel>[propModel];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Defines the format in which an [ReflectionInfoModel] is expressed as Dart
|
/// Defines the format in which an [ReflectionInfoModel] is expressed as Dart
|
||||||
/// code in a `.ng_deps.dart` file.
|
/// code in a `.ng_deps.dart` file.
|
||||||
abstract class ReflectionWriterMixin
|
abstract class ReflectionWriterMixin
|
||||||
|
@ -171,9 +260,25 @@ abstract class ReflectionWriterMixin
|
||||||
_writeListWithSeparator(model.parameters, writeParameterModelForImpl,
|
_writeListWithSeparator(model.parameters, writeParameterModelForImpl,
|
||||||
prefix: '(', suffix: ')');
|
prefix: '(', suffix: ')');
|
||||||
// Interfaces
|
// Interfaces
|
||||||
|
var hasPropertyMetadata =
|
||||||
|
model.propertyMetadata != null && model.propertyMetadata.isNotEmpty;
|
||||||
if (model.interfaces != null && model.interfaces.isNotEmpty) {
|
if (model.interfaces != null && model.interfaces.isNotEmpty) {
|
||||||
_writeListWithSeparator(model.interfaces, buffer.write,
|
_writeListWithSeparator(model.interfaces, buffer.write,
|
||||||
prefix: ',\nconst [', suffix: ']');
|
prefix: ',\nconst [', suffix: ']');
|
||||||
|
} else if (hasPropertyMetadata) {
|
||||||
|
buffer.write(',\nconst []');
|
||||||
|
}
|
||||||
|
// Property Metadata
|
||||||
|
if (hasPropertyMetadata) {
|
||||||
|
buffer.write(',\nconst {');
|
||||||
|
for (var propMeta in model.propertyMetadata) {
|
||||||
|
if (propMeta != model.propertyMetadata.first) {
|
||||||
|
buffer.write(', ');
|
||||||
|
}
|
||||||
|
_writeListWithSeparator(propMeta.annotations, writeAnnotationModel,
|
||||||
|
prefix: "\n'${sanitize(propMeta.name)}': const [", suffix: ']');
|
||||||
|
}
|
||||||
|
buffer.write('}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
buffer.writeln(')\n)');
|
buffer.writeln(')\n)');
|
||||||
|
|
|
@ -7,6 +7,38 @@ import 'package:protobuf/protobuf.dart';
|
||||||
import 'annotation_model.pb.dart';
|
import 'annotation_model.pb.dart';
|
||||||
import 'parameter_model.pb.dart';
|
import 'parameter_model.pb.dart';
|
||||||
|
|
||||||
|
class PropertyMetadataModel extends GeneratedMessage {
|
||||||
|
static final BuilderInfo _i = new BuilderInfo('PropertyMetadataModel')
|
||||||
|
..a(1, 'name', PbFieldType.QS)
|
||||||
|
..pp(2, 'annotations', PbFieldType.PM, AnnotationModel.$checkItem, AnnotationModel.create)
|
||||||
|
;
|
||||||
|
|
||||||
|
PropertyMetadataModel() : super();
|
||||||
|
PropertyMetadataModel.fromBuffer(List<int> i, [ExtensionRegistry r = ExtensionRegistry.EMPTY]) : super.fromBuffer(i, r);
|
||||||
|
PropertyMetadataModel.fromJson(String i, [ExtensionRegistry r = ExtensionRegistry.EMPTY]) : super.fromJson(i, r);
|
||||||
|
PropertyMetadataModel clone() => new PropertyMetadataModel()..mergeFromMessage(this);
|
||||||
|
BuilderInfo get info_ => _i;
|
||||||
|
static PropertyMetadataModel create() => new PropertyMetadataModel();
|
||||||
|
static PbList<PropertyMetadataModel> createRepeated() => new PbList<PropertyMetadataModel>();
|
||||||
|
static PropertyMetadataModel getDefault() {
|
||||||
|
if (_defaultInstance == null) _defaultInstance = new _ReadonlyPropertyMetadataModel();
|
||||||
|
return _defaultInstance;
|
||||||
|
}
|
||||||
|
static PropertyMetadataModel _defaultInstance;
|
||||||
|
static void $checkItem(PropertyMetadataModel v) {
|
||||||
|
if (v is !PropertyMetadataModel) checkItemFailed(v, 'PropertyMetadataModel');
|
||||||
|
}
|
||||||
|
|
||||||
|
String get name => getField(1);
|
||||||
|
void set name(String v) { setField(1, v); }
|
||||||
|
bool hasName() => hasField(1);
|
||||||
|
void clearName() => clearField(1);
|
||||||
|
|
||||||
|
List<AnnotationModel> get annotations => getField(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
class _ReadonlyPropertyMetadataModel extends PropertyMetadataModel with ReadonlyMessageMixin {}
|
||||||
|
|
||||||
class ReflectionInfoModel extends GeneratedMessage {
|
class ReflectionInfoModel extends GeneratedMessage {
|
||||||
static final BuilderInfo _i = new BuilderInfo('ReflectionInfoModel')
|
static final BuilderInfo _i = new BuilderInfo('ReflectionInfoModel')
|
||||||
..a(1, 'name', PbFieldType.QS)
|
..a(1, 'name', PbFieldType.QS)
|
||||||
|
@ -15,6 +47,7 @@ class ReflectionInfoModel extends GeneratedMessage {
|
||||||
..pp(4, 'annotations', PbFieldType.PM, AnnotationModel.$checkItem, AnnotationModel.create)
|
..pp(4, 'annotations', PbFieldType.PM, AnnotationModel.$checkItem, AnnotationModel.create)
|
||||||
..pp(5, 'parameters', PbFieldType.PM, ParameterModel.$checkItem, ParameterModel.create)
|
..pp(5, 'parameters', PbFieldType.PM, ParameterModel.$checkItem, ParameterModel.create)
|
||||||
..p(6, 'interfaces', PbFieldType.PS)
|
..p(6, 'interfaces', PbFieldType.PS)
|
||||||
|
..pp(7, 'propertyMetadata', PbFieldType.PM, PropertyMetadataModel.$checkItem, PropertyMetadataModel.create)
|
||||||
;
|
;
|
||||||
|
|
||||||
ReflectionInfoModel() : super();
|
ReflectionInfoModel() : super();
|
||||||
|
@ -53,10 +86,20 @@ class ReflectionInfoModel extends GeneratedMessage {
|
||||||
List<ParameterModel> get parameters => getField(5);
|
List<ParameterModel> get parameters => getField(5);
|
||||||
|
|
||||||
List<String> get interfaces => getField(6);
|
List<String> get interfaces => getField(6);
|
||||||
|
|
||||||
|
List<PropertyMetadataModel> get propertyMetadata => getField(7);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ReadonlyReflectionInfoModel extends ReflectionInfoModel with ReadonlyMessageMixin {}
|
class _ReadonlyReflectionInfoModel extends ReflectionInfoModel with ReadonlyMessageMixin {}
|
||||||
|
|
||||||
|
const PropertyMetadataModel$json = const {
|
||||||
|
'1': 'PropertyMetadataModel',
|
||||||
|
'2': const [
|
||||||
|
const {'1': 'name', '3': 1, '4': 2, '5': 9},
|
||||||
|
const {'1': 'annotations', '3': 2, '4': 3, '5': 11, '6': '.angular2.src.transform.common.model.proto.AnnotationModel'},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
const ReflectionInfoModel$json = const {
|
const ReflectionInfoModel$json = const {
|
||||||
'1': 'ReflectionInfoModel',
|
'1': 'ReflectionInfoModel',
|
||||||
'2': const [
|
'2': const [
|
||||||
|
@ -66,12 +109,13 @@ const ReflectionInfoModel$json = const {
|
||||||
const {'1': 'annotations', '3': 4, '4': 3, '5': 11, '6': '.angular2.src.transform.common.model.proto.AnnotationModel'},
|
const {'1': 'annotations', '3': 4, '4': 3, '5': 11, '6': '.angular2.src.transform.common.model.proto.AnnotationModel'},
|
||||||
const {'1': 'parameters', '3': 5, '4': 3, '5': 11, '6': '.angular2.src.transform.common.model.proto.ParameterModel'},
|
const {'1': 'parameters', '3': 5, '4': 3, '5': 11, '6': '.angular2.src.transform.common.model.proto.ParameterModel'},
|
||||||
const {'1': 'interfaces', '3': 6, '4': 3, '5': 9},
|
const {'1': 'interfaces', '3': 6, '4': 3, '5': 9},
|
||||||
|
const {'1': 'propertyMetadata', '3': 7, '4': 3, '5': 11, '6': '.angular2.src.transform.common.model.proto.PropertyMetadataModel'},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generated with:
|
* Generated with:
|
||||||
* reflection_info_model.proto (fd01d8a29e6bccccc343ef975829fd7cb6a63312)
|
* reflection_info_model.proto (71d723738054f1276f792a2672a956ef9be94a4c)
|
||||||
* libprotoc 2.5.0
|
* libprotoc 2.5.0
|
||||||
* dart-protoc-plugin (cc35f743de982a4916588b9c505dd21c7fe87d17)
|
* dart-protoc-plugin (cc35f743de982a4916588b9c505dd21c7fe87d17)
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -5,6 +5,14 @@ import "parameter_model.proto";
|
||||||
|
|
||||||
package angular2.src.transform.common.model.proto;
|
package angular2.src.transform.common.model.proto;
|
||||||
|
|
||||||
|
message PropertyMetadataModel {
|
||||||
|
// The name of the property with metadata attached.
|
||||||
|
required string name = 1;
|
||||||
|
|
||||||
|
// The metadata attached to the property.
|
||||||
|
repeated AnnotationModel annotations = 2;
|
||||||
|
}
|
||||||
|
|
||||||
message ReflectionInfoModel {
|
message ReflectionInfoModel {
|
||||||
// The (potentially prefixed) name of this Injectable.
|
// The (potentially prefixed) name of this Injectable.
|
||||||
// This can be a `Type` or a function name.
|
// This can be a `Type` or a function name.
|
||||||
|
@ -21,4 +29,7 @@ message ReflectionInfoModel {
|
||||||
repeated ParameterModel parameters = 5;
|
repeated ParameterModel parameters = 5;
|
||||||
|
|
||||||
repeated string interfaces = 6;
|
repeated string interfaces = 6;
|
||||||
|
|
||||||
|
// Entries for all properties with associated metadata.
|
||||||
|
repeated PropertyMetadataModel propertyMetadata = 7;
|
||||||
}
|
}
|
||||||
|
|
|
@ -292,6 +292,61 @@ void allTests() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('property metadata', () {
|
||||||
|
it('should be recorded on fields', () async {
|
||||||
|
var model = await _testCreateModel('prop_metadata_files/fields.dart');
|
||||||
|
|
||||||
|
expect(model.reflectables.first.propertyMetadata).toBeNotNull();
|
||||||
|
expect(model.reflectables.first.propertyMetadata.isNotEmpty).toBeTrue();
|
||||||
|
expect(model.reflectables.first.propertyMetadata.first.name)
|
||||||
|
.toEqual('field');
|
||||||
|
expect(model.reflectables.first.propertyMetadata.first.annotations
|
||||||
|
.firstWhere((a) => a.name == 'FieldDecorator',
|
||||||
|
orElse: () => null)).toBeNotNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be recorded on getters', () async {
|
||||||
|
var model = await _testCreateModel('prop_metadata_files/getters.dart');
|
||||||
|
|
||||||
|
expect(model.reflectables.first.propertyMetadata).toBeNotNull();
|
||||||
|
expect(model.reflectables.first.propertyMetadata.isNotEmpty).toBeTrue();
|
||||||
|
expect(model.reflectables.first.propertyMetadata.first.name)
|
||||||
|
.toEqual('getVal');
|
||||||
|
expect(model.reflectables.first.propertyMetadata.first.annotations
|
||||||
|
.firstWhere((a) => a.name == 'GetDecorator', orElse: () => null))
|
||||||
|
.toBeNotNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be recorded on setters', () async {
|
||||||
|
var model = await _testCreateModel('prop_metadata_files/setters.dart');
|
||||||
|
|
||||||
|
expect(model.reflectables.first.propertyMetadata).toBeNotNull();
|
||||||
|
expect(model.reflectables.first.propertyMetadata.isNotEmpty).toBeTrue();
|
||||||
|
expect(model.reflectables.first.propertyMetadata.first.name)
|
||||||
|
.toEqual('setVal');
|
||||||
|
expect(model.reflectables.first.propertyMetadata.first.annotations
|
||||||
|
.firstWhere((a) => a.name == 'SetDecorator', orElse: () => null))
|
||||||
|
.toBeNotNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be coalesced when getters and setters have the same name',
|
||||||
|
() async {
|
||||||
|
var model = await _testCreateModel(
|
||||||
|
'prop_metadata_files/getters_and_setters.dart');
|
||||||
|
|
||||||
|
expect(model.reflectables.first.propertyMetadata).toBeNotNull();
|
||||||
|
expect(model.reflectables.first.propertyMetadata.length).toBe(1);
|
||||||
|
expect(model.reflectables.first.propertyMetadata.first.name)
|
||||||
|
.toEqual('myVal');
|
||||||
|
expect(model.reflectables.first.propertyMetadata.first.annotations
|
||||||
|
.firstWhere((a) => a.name == 'GetDecorator', orElse: () => null))
|
||||||
|
.toBeNotNull();
|
||||||
|
expect(model.reflectables.first.propertyMetadata.first.annotations
|
||||||
|
.firstWhere((a) => a.name == 'SetDecorator', orElse: () => null))
|
||||||
|
.toBeNotNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should not throw/hang on invalid urls', () async {
|
it('should not throw/hang on invalid urls', () async {
|
||||||
var logger = new RecordingLogger();
|
var logger = new RecordingLogger();
|
||||||
var model =
|
var model =
|
||||||
|
|
|
@ -0,0 +1,8 @@
|
||||||
|
library fields;
|
||||||
|
|
||||||
|
import 'package:angular2/src/core/metadata.dart';
|
||||||
|
|
||||||
|
@Component(selector: '[fields]')
|
||||||
|
class FieldComponent {
|
||||||
|
@FieldDecorator("field") String field;
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
library fields;
|
||||||
|
|
||||||
|
import 'package:angular2/src/core/metadata.dart';
|
||||||
|
|
||||||
|
@Component(selector: '[getters]')
|
||||||
|
class FieldComponent {
|
||||||
|
@GetDecorator("get") String get getVal => 'a';
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
library fields;
|
||||||
|
|
||||||
|
import 'package:angular2/src/core/metadata.dart';
|
||||||
|
|
||||||
|
@Component(selector: '[getters-and-setters]')
|
||||||
|
class FieldComponent {
|
||||||
|
String _val;
|
||||||
|
@GetDecorator("get") String get myVal => _val;
|
||||||
|
@SetDecorator("set") String set myVal(val) => _val = val;
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
library fields;
|
||||||
|
|
||||||
|
import 'package:angular2/src/core/metadata.dart';
|
||||||
|
|
||||||
|
@Component(selector: '[setters]')
|
||||||
|
class FieldComponent {
|
||||||
|
@SetDecorator("set") String set setVal(val) => null;
|
||||||
|
}
|
Loading…
Reference in New Issue