Skip to content
This repository was archived by the owner on Jun 20, 2019. It is now read-only.

Commit 593afa4

Browse files
Best current solution to handle all types of generic directives refs #86 (#95)
* Best current solution to handle all types of generic directives refs #86 In the future, maybe there's a way to know the directive's parameters based on how its used in the template. For now, use the upper bounds because those are the lowest types and therefore can be relied upon in the template, and avoids the 'T is not subtype of String' errors. Refactoring: changed event typechecking to go off of a new type field on InputElement so that it could be deparameterized consistently at collection time rather than deparameterized everywhere its used. Same for outputs, but those already had an eventType field that was used everywhere. Opening new issue #91 for some tricky subtleties in generic chains. Looks like this code should some day use `instatiateToBounds` but that will not do much typing without strong mode turned on, and has a bug that is open in their repo. * Review feedback from jmesserly
1 parent 67960cc commit 593afa4

File tree

5 files changed

+350
-23
lines changed

5 files changed

+350
-23
lines changed

analyzer_plugin/lib/src/model.dart

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,8 @@ class InputElement extends AngularElementImpl {
172172

173173
final dart.PropertyAccessorElement setter;
174174

175+
final dart.DartType setterType;
176+
175177
/**
176178
* The [SourceRange] where [setter] is referenced in the input declaration.
177179
* May be the same as this element offset/length in shorthand variants where
@@ -180,7 +182,7 @@ class InputElement extends AngularElementImpl {
180182
final SourceRange setterRange;
181183

182184
InputElement(String name, int nameOffset, int nameLength, Source source,
183-
this.setter, this.setterRange)
185+
this.setter, this.setterRange, this.setterType)
184186
: super(name, nameOffset, nameLength, source);
185187

186188
@override

analyzer_plugin/lib/src/resolver.dart

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -698,12 +698,15 @@ class TemplateResolver {
698698
AngularWarningCode unboundErrorCode;
699699
var matched = false;
700700
if (attribute.bound == AttributeBoundType.output) {
701+
// Set the event type to dynamic, for if we don't match anything
702+
eventType = typeProvider.dynamicType;
701703
unboundErrorCode = AngularWarningCode.NONEXIST_OUTPUT_BOUND;
702704
for (AbstractDirective directive in directives) {
703705
for (OutputElement output in directive.outputs) {
706+
// TODO what if this matches two directives?
704707
if (output.name == attribute.propertyName) {
705-
// TODO what if this matches two directives?
706708
eventType = output.eventType;
709+
// Parameterized directives, use the lower bound
707710
matched = true;
708711
}
709712
}
@@ -733,7 +736,9 @@ class TemplateResolver {
733736
for (OutputElement output in directive.outputs) {
734737
if (output.name == attribute.propertyName + "Change") {
735738
outputMatched = true;
736-
if (!output.eventType.isAssignableTo(expression.bestType)) {
739+
var eventType = output.eventType;
740+
741+
if (!eventType.isAssignableTo(expression.bestType)) {
737742
errorListener.onError(new AnalysisError(
738743
templateSource,
739744
attribute.valueOffset,
@@ -770,7 +775,8 @@ class TemplateResolver {
770775
for (InputElement input in directive.inputs) {
771776
if (input.name == attribute.propertyName) {
772777
var attrType = expression.bestType;
773-
var inputType = input.setter.variable.type;
778+
var inputType = input.setterType;
779+
774780
if (!attrType.isAssignableTo(inputType)) {
775781
errorListener.onError(new AnalysisError(
776782
templateSource,

analyzer_plugin/lib/src/tasks.dart

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -406,8 +406,14 @@ class BuildUnitDirectivesTask extends SourceBasedAnalysisTask
406406
PropertyAccessorElement setter =
407407
_resolveSetter(classElement, expression, name);
408408

409-
return new InputElement(boundName, boundRange.offset, boundRange.length,
410-
target.source, setter, nameRange);
409+
return new InputElement(
410+
boundName,
411+
boundRange.offset,
412+
boundRange.length,
413+
target.source,
414+
setter,
415+
nameRange,
416+
_getSetterType(classElement, setter));
411417
} else {
412418
// TODO(mfairhurst) report a warning
413419
return null;
@@ -427,7 +433,7 @@ class BuildUnitDirectivesTask extends SourceBasedAnalysisTask
427433
PropertyAccessorElement getter =
428434
_resolveGetter(classElement, expression, name);
429435

430-
var eventType = getEventType(getter, name);
436+
var eventType = getEventType(classElement, getter, name);
431437

432438
return new OutputElement(boundName, boundRange.offset, boundRange.length,
433439
target.source, getter, nameRange, eventType);
@@ -437,15 +443,26 @@ class BuildUnitDirectivesTask extends SourceBasedAnalysisTask
437443
}
438444
}
439445

440-
DartType getEventType(PropertyAccessorElement getter, String name) {
446+
DartType _getSetterType(
447+
ClassElement classElement, PropertyAccessorElement setter) {
448+
if (setter != null && setter.variable != null) {
449+
var type = setter.variable.type;
450+
return _deparameterizeType(classElement, type);
451+
}
452+
453+
return null;
454+
}
455+
456+
DartType getEventType(
457+
ClassElement classElement, PropertyAccessorElement getter, String name) {
441458
if (getter != null && getter.type != null) {
442459
var returnType = getter.type.returnType;
443460
if (returnType != null && returnType is InterfaceType) {
444461
dart.DartType streamType = typeProvider.streamType;
445462
dart.DartType streamedType =
446463
context.typeSystem.mostSpecificTypeArgument(returnType, streamType);
447464
if (streamedType != null) {
448-
return streamedType;
465+
return _deparameterizeType(classElement, streamedType);
449466
} else {
450467
errorReporter.reportErrorForNode(
451468
AngularWarningCode.OUTPUT_MUST_BE_EVENTEMITTER,
@@ -463,6 +480,25 @@ class BuildUnitDirectivesTask extends SourceBasedAnalysisTask
463480
return typeProvider.dynamicType;
464481
}
465482

483+
DartType _deparameterizeType(
484+
ClassElement classElement, DartType parameterizedType) {
485+
if (parameterizedType == null) {
486+
return null;
487+
}
488+
489+
// See #91 for discussion about bugs related to bounds
490+
var getBound = (p) {
491+
return p.bound == null
492+
? typeProvider.dynamicType
493+
: p.bound.resolveToBound(typeProvider.dynamicType);
494+
};
495+
496+
var getType = (p) => p.type;
497+
var bounds = classElement.typeParameters.map(getBound).toList();
498+
var parameters = classElement.typeParameters.map(getType).toList();
499+
return parameterizedType.substitute2(bounds, parameters);
500+
}
501+
466502
List<InputElement> _parseHeaderInputs(
467503
ClassElement classElement, ast.Annotation node) {
468504
ast.ListLiteral descList = _getListLiteralNamedArgument(
@@ -504,8 +540,12 @@ class BuildUnitDirectivesTask extends SourceBasedAnalysisTask
504540
* the given `@Input` or `@Output` [annotation], and add it to the
505541
* [inputElements] or [outputElements] array.
506542
*/
507-
_parseMemberInputOrOutput(ast.ClassMember node, ast.Annotation annotation,
508-
List<InputElement> inputElements, List<OutputElement> outputElements) {
543+
_parseMemberInputOrOutput(
544+
ClassElement classElement,
545+
ast.ClassMember node,
546+
ast.Annotation annotation,
547+
List<InputElement> inputElements,
548+
List<OutputElement> outputElements) {
509549
// analyze the annotation
510550
final isInput = _isAngularAnnotation(annotation, 'Input');
511551
final isOutput = _isAngularAnnotation(annotation, 'Output');
@@ -568,9 +608,15 @@ class BuildUnitDirectivesTask extends SourceBasedAnalysisTask
568608

569609
if (isInput) {
570610
inputElements.add(new InputElement(
571-
name, nameOffset, nameLength, target.source, property, null));
611+
name,
612+
nameOffset,
613+
nameLength,
614+
target.source,
615+
property,
616+
null,
617+
_getSetterType(classElement, property)));
572618
} else {
573-
var eventType = getEventType(property, name);
619+
var eventType = getEventType(classElement, property, name);
574620
outputElements.add(new OutputElement(name, nameOffset, nameLength,
575621
target.source, property, null, eventType));
576622
}
@@ -585,7 +631,7 @@ class BuildUnitDirectivesTask extends SourceBasedAnalysisTask
585631
for (ast.ClassMember member in node.members) {
586632
for (ast.Annotation annotation in member.metadata) {
587633
_parseMemberInputOrOutput(
588-
member, annotation, inputElements, outputElements);
634+
node.element, member, annotation, inputElements, outputElements);
589635
}
590636
}
591637
}
@@ -1369,8 +1415,14 @@ class _BuildStandardHtmlComponentsVisitor extends RecursiveAstVisitor {
13691415
String name = accessor.displayName;
13701416
if (!inputMap.containsKey(name)) {
13711417
if (accessor.isSetter) {
1372-
inputMap[name] = new InputElement(name, accessor.nameOffset,
1373-
accessor.nameLength, accessor.source, accessor, null);
1418+
inputMap[name] = new InputElement(
1419+
name,
1420+
accessor.nameOffset,
1421+
accessor.nameLength,
1422+
accessor.source,
1423+
accessor,
1424+
null,
1425+
accessor.variable.type);
13741426
}
13751427
}
13761428
});

analyzer_plugin/test/resolver_test.dart

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -552,6 +552,184 @@ class TestPanel {
552552
AngularWarningCode.CSS_UNIT_BINDING_NOT_NUMBER, code, "notNumber");
553553
}
554554

555+
void test_expression_inputAndOutputBinding_genericDirective_ok() {
556+
_addDartSource(r'''
557+
@Component(selector: 'test-panel',
558+
directives: const [GenericComponent], templateUrl: 'test_panel.html')
559+
class TestPanel {
560+
String string;
561+
}
562+
@Component(selector: 'generic-comp', template: '')
563+
class GenericComponent<T> {
564+
@Output() EventEmitter<T> output;
565+
@Input() T input;
566+
567+
@Output() EventEmitter<T> twoWayChange;
568+
@Input() T twoWay;
569+
}
570+
''');
571+
var code = r"""
572+
<generic-comp (output)='$event.length' [input]="string" [(twoWay)]="string"></generic-comp>
573+
""";
574+
_addHtmlSource(code);
575+
_resolveSingleTemplate(dartSource);
576+
errorListener.assertNoErrors();
577+
}
578+
579+
void test_expression_inputAndOutputBinding_genericDirective_chain_ok() {
580+
_addDartSource(r'''
581+
@Component(selector: 'test-panel',
582+
directives: const [GenericComponent], templateUrl: 'test_panel.html')
583+
class TestPanel {
584+
String string;
585+
}
586+
@Component(selector: 'generic-comp', template: '')
587+
class GenericComponent<T extends E, E> {
588+
@Output() EventEmitter<T> output;
589+
@Input() T input;
590+
591+
@Output() EventEmitter<T> twoWayChange;
592+
@Input() T twoWay;
593+
}
594+
''');
595+
var code = r"""
596+
<generic-comp (output)='$event.length' [input]="string" [(twoWay)]="string"></generic-comp>
597+
""";
598+
_addHtmlSource(code);
599+
_resolveSingleTemplate(dartSource);
600+
errorListener.assertNoErrors();
601+
}
602+
603+
void test_expression_inputAndOutputBinding_genericDirective_nested_ok() {
604+
_addDartSource(r'''
605+
@Component(selector: 'test-panel',
606+
directives: const [GenericComponent], templateUrl: 'test_panel.html')
607+
class TestPanel {
608+
List<String> stringList;
609+
}
610+
@Component(selector: 'generic-comp', template: '')
611+
class GenericComponent<T> {
612+
@Output() EventEmitter<List<T>> output;
613+
@Input() List<T> input;
614+
615+
@Output() EventEmitter<List<T>> twoWayChange;
616+
@Input() List<T> twoWay;
617+
}
618+
''');
619+
var code = r"""
620+
<generic-comp (output)='$event[0].length' [input]="stringList" [(twoWay)]="stringList"></generic-comp>
621+
""";
622+
_addHtmlSource(code);
623+
_resolveSingleTemplate(dartSource);
624+
errorListener.assertNoErrors();
625+
}
626+
627+
void test_expression_inputBinding_genericDirective_lowerBoundTypeError() {
628+
_addDartSource(r'''
629+
@Component(selector: 'test-panel',
630+
directives: const [GenericComponent], templateUrl: 'test_panel.html')
631+
class TestPanel {
632+
int notString;
633+
}
634+
@Component(selector: 'generic-comp', template: '')
635+
class GenericComponent<T extends String> {
636+
@Input() T string;
637+
}
638+
''');
639+
var code = r"""
640+
<generic-comp [string]="notString"></generic-comp>
641+
""";
642+
_addHtmlSource(code);
643+
_resolveSingleTemplate(dartSource);
644+
assertErrorInCodeAtPosition(
645+
AngularWarningCode.INPUT_BINDING_TYPE_ERROR, code, "notString");
646+
}
647+
648+
void test_expression_input_genericDirective_lowerBoundChainTypeError() {
649+
_addDartSource(r'''
650+
@Component(selector: 'test-panel',
651+
directives: const [GenericComponent], templateUrl: 'test_panel.html')
652+
class TestPanel {
653+
int notString;
654+
}
655+
@Component(selector: 'generic-comp', template: '')
656+
class GenericComponent<T extends O, O extends String> {
657+
@Input() T string;
658+
}
659+
''');
660+
var code = r"""
661+
<generic-comp [string]="notString"></generic-comp>
662+
""";
663+
_addHtmlSource(code);
664+
_resolveSingleTemplate(dartSource);
665+
assertErrorInCodeAtPosition(
666+
AngularWarningCode.INPUT_BINDING_TYPE_ERROR, code, "notString");
667+
}
668+
669+
void test_expression_input_genericDirective_lowerBoundNestedTypeError() {
670+
_addDartSource(r'''
671+
@Component(selector: 'test-panel',
672+
directives: const [GenericComponent], templateUrl: 'test_panel.html')
673+
class TestPanel {
674+
List<int> notStringList;
675+
}
676+
@Component(selector: 'generic-comp', template: '')
677+
class GenericComponent<T extends String> {
678+
@Input() List<T> stringList;
679+
}
680+
''');
681+
var code = r"""
682+
<generic-comp [stringList]="notStringList"></generic-comp>
683+
""";
684+
_addHtmlSource(code);
685+
_resolveSingleTemplate(dartSource);
686+
assertErrorInCodeAtPosition(
687+
AngularWarningCode.INPUT_BINDING_TYPE_ERROR, code, "notStringList");
688+
}
689+
690+
void test_expression_outputBinding_genericDirective_lowerBoundTypeError() {
691+
_addDartSource(r'''
692+
@Component(selector: 'test-panel',
693+
directives: const [GenericComponent], templateUrl: 'test_panel.html')
694+
class TestPanel {
695+
takeInt(int i) {}
696+
}
697+
@Component(selector: 'generic-comp', template: '')
698+
class GenericComponent<T extends String> {
699+
@Output() EventEmitter<T> string;
700+
}
701+
''');
702+
var code = r"""
703+
<generic-comp (string)="takeInt($event)"></generic-comp>
704+
""";
705+
_addHtmlSource(code);
706+
_resolveSingleTemplate(dartSource);
707+
assertErrorInCodeAtPosition(
708+
StaticWarningCode.ARGUMENT_TYPE_NOT_ASSIGNABLE, code, r"$event");
709+
}
710+
711+
void test_expression_twoWayBinding_genericDirective_lowerBoundTypeError() {
712+
_addDartSource(r'''
713+
@Component(selector: 'test-panel',
714+
directives: const [GenericComponent], templateUrl: 'test_panel.html')
715+
class TestPanel {
716+
int anInt;
717+
}
718+
@Component(selector: 'generic-comp', template: '')
719+
class GenericComponent<T extends String> {
720+
@Output() EventEmitter<T> stringChange;
721+
@Input() dynamic string;
722+
}
723+
''');
724+
var code = r"""
725+
<generic-comp [(string)]="anInt"></generic-comp>
726+
""";
727+
_addHtmlSource(code);
728+
_resolveSingleTemplate(dartSource);
729+
assertErrorInCodeAtPosition(
730+
AngularWarningCode.TWO_WAY_BINDING_OUTPUT_TYPE_ERROR, code, "anInt");
731+
}
732+
555733
void test_inheritedFields() {
556734
_addDartSource(r'''
557735
class BaseComponent {

0 commit comments

Comments
 (0)