Check reductions are only on keyup keydown (#430)

* Only allow event reductions on keyup and keydown

This is the broadest type of validation that we can do easily.
Shouldn't introduce any false errors.

Not as complete as validating the available keys and modifiers and
their ordering, but that is less important than warning people about
when reductions apply or don't apply at all.

* Add `(keyup.ctrl.space)` to the features grid

* Tests for StatementsAttr reductions offset/len. Kill a .swp file.
diff --git a/README.md b/README.md
index e5514f1..a97fe21 100644
--- a/README.md
+++ b/README.md
@@ -91,6 +91,7 @@
 `<div [class.extra-sparkle]="isDelightful">` | :white_check_mark: validity of clasname, soundness of expression, type of expression must be bool | :last_quarter_moon: complete inside binding but binding not suggested | :last_quarter_moon: no css specific navigation | :x:
 `<div [style.width.px]="mySize">` | :waning_gibbous_moon: soundness of expression, css properties are generally checked but not against a dictionary, same for units, expression must type to `int` if units are present | :last_quarter_moon: complete inside binding but binding not suggested | :last_quarter_moon: no css specific navigation | :x:
 `<button (click)="readRainbow($event)">` | :white_check_mark: soundness of expression, type of `$event`, existence of output on component/element and DOM events which propagate can be tracked anywhere | :white_check_mark: | :last_quarter_moon: no navigation for `$event` | :x:
+`<button (keyup.enter)="...">` | :waning_gibbous_moon: soundness of expression, type of `$event`, keycode and modifier names not checked | :white_check_mark: | :last_quarter_moon: no navigation for `$event` | :x:
 `<button on-click="readRainbow($event)">` | :white_check_mark: | :skull: | :last_quarter_moon: no navigation for `$event` | :x:
 `<div title="Hello {{ponyName}}">` | :white_check_mark: soundness of expression, matching mustache delimiters | :white_check_mark: | :white_check_mark: | :x:
 `<p>Hello {{ponyName}}</p>` | :white_check_mark: soundness of expression, matching mustache delimiters | :white_check_mark: | :white_check_mark: | :x:
diff --git a/angular_analyzer_plugin/lib/ast.dart b/angular_analyzer_plugin/lib/ast.dart
index fff88d3..bec4804 100644
--- a/angular_analyzer_plugin/lib/ast.dart
+++ b/angular_analyzer_plugin/lib/ast.dart
@@ -176,6 +176,11 @@
   @override
   String toString() => '(${super.toString()}, [$statements])';
 
+  int get reductionsOffset =>
+      reductions.isEmpty ? null : nameOffset + name.length;
+  int get reductionsLength =>
+      reductions.isEmpty ? null : '.'.length + reductions.join('.').length;
+
   @override
   void accept(AngularAstVisitor visitor) =>
       visitor.visitStatementsBoundAttr(this);
diff --git a/angular_analyzer_plugin/lib/errors.dart b/angular_analyzer_plugin/lib/errors.dart
index e92230b..fba3e11 100644
--- a/angular_analyzer_plugin/lib/errors.dart
+++ b/angular_analyzer_plugin/lib/errors.dart
@@ -60,6 +60,7 @@
   AngularWarningCode.PIPE_TRANSFORM_NO_NAMED_ARGS,
   AngularWarningCode.PIPE_TRANSFORM_REQ_ONE_ARG,
   AngularWarningCode.UNSAFE_BINDING,
+  AngularWarningCode.EVENT_REDUCTION_NOT_ALLOWED,
 ];
 
 /// The lazy initialized map from [AngularWarningCode.uniqueName] to the
@@ -437,6 +438,12 @@
       'A security exception will be thrown by this binding. You must use the '
       ' security service to get an instance of {0} and bind that result.');
 
+  /// An error indicating that an event other than `(keyup.x)` or `(keydown.+)`
+  /// etc, had reduction suffixes in that style, which is not allowed.
+  static const EVENT_REDUCTION_NOT_ALLOWED = const AngularWarningCode(
+      'EVENT_REDUCTION_NOT_ALLOWED',
+      'Event reductions are only allowed on keyup and keydown events');
+
   /// Initialize a newly created error code to have the given [name].
   /// The message associated with the error will be created from the given
   /// [message] template. The correction associated with the error will be
diff --git a/angular_analyzer_plugin/lib/src/resolver.dart b/angular_analyzer_plugin/lib/src/resolver.dart
index 788ab72..59860bd 100644
--- a/angular_analyzer_plugin/lib/src/resolver.dart
+++ b/angular_analyzer_plugin/lib/src/resolver.dart
@@ -681,6 +681,16 @@
 
   @override
   void visitStatementsBoundAttr(StatementsBoundAttribute attr) {
+    if (attr.reductions.isNotEmpty &&
+        attr.name != 'keyup' &&
+        attr.name != 'keydown') {
+      errorListener.onError(new AnalysisError(
+          templateSource,
+          attr.reductionsOffset,
+          attr.reductionsLength,
+          AngularWarningCode.EVENT_REDUCTION_NOT_ALLOWED));
+    }
+
     var eventType = typeProvider.dynamicType;
     var matched = false;
 
diff --git a/angular_analyzer_plugin/test/.plugin_test.dart.swp b/angular_analyzer_plugin/test/.plugin_test.dart.swp
deleted file mode 100644
index d34ed4a..0000000
--- a/angular_analyzer_plugin/test/.plugin_test.dart.swp
+++ /dev/null
Binary files differ
diff --git a/angular_analyzer_plugin/test/ast.dart b/angular_analyzer_plugin/test/ast.dart
new file mode 100644
index 0000000..70fc48e
--- /dev/null
+++ b/angular_analyzer_plugin/test/ast.dart
@@ -0,0 +1,60 @@
+import 'package:angular_analyzer_plugin/ast.dart';
+import 'package:test_reflective_loader/test_reflective_loader.dart';
+import 'package:test/test.dart';
+
+void main() {
+  defineReflectiveSuite(() {
+    defineReflectiveTests(StatementsBoundAttrTest);
+  });
+}
+
+@reflectiveTest
+class StatementsBoundAttrTest {
+  // ignore: non_constant_identifier_names
+  void test_reductionOffsetNoReductions() {
+    final attr = new StatementsBoundAttribute(
+        'name',
+        '('.length, // nameOffset
+        'value',
+        '(name)="'.length, //valueOffset
+        '(name)',
+        0, // originalNameOffset
+        [], // reductions
+        [] // statements
+        );
+    expect(attr.reductionsOffset, null);
+    expect(attr.reductionsLength, null);
+  }
+
+  // ignore: non_constant_identifier_names
+  void test_reductionOffsetOneReduction() {
+    final attr = new StatementsBoundAttribute(
+        'name',
+        '('.length, // nameOffset
+        'value',
+        '(name.reduction)="'.length, //valueOffset
+        '(name)',
+        0, // originalNameOffset
+        ['reduction'], // reductions
+        [] // statements
+        );
+    expect(attr.reductionsOffset, '(name'.length);
+    expect(attr.reductionsLength, '.reduction'.length);
+  }
+
+  // ignore: non_constant_identifier_names
+  void test_reductionOffsetMultipleReductions() {
+    final attr = new StatementsBoundAttribute(
+        'name',
+        '('.length, // nameOffset
+        'value',
+        '(name.reductions.here)="'.length, //valueOffset
+        '(name)',
+        0, // originalNameOffset
+        ['reductions', 'here'], // reductions
+        [] // statements
+        );
+    expect(attr.reductionsOffset, '(name'.length);
+    expect(attr.reductionsLength, '.reductions.here'.length);
+  }
+}
diff --git a/angular_analyzer_plugin/test/resolver_test.dart b/angular_analyzer_plugin/test/resolver_test.dart
index fb66650..6d182ff 100644
--- a/angular_analyzer_plugin/test/resolver_test.dart
+++ b/angular_analyzer_plugin/test/resolver_test.dart
@@ -128,6 +128,26 @@
   }
 
   // ignore: non_constant_identifier_names
+  Future test_expression_reductionsOnRegularOutputsNotAllowed() async {
+    _addDartSource(r'''
+import 'dart:html';
+@Component(selector: 'test-panel')
+@View(templateUrl: 'test_panel.html')
+class TestPanel {
+  void handle(dynamic e) {
+  }
+}
+''');
+    var code = r'''
+<div (click.whatever)='handle($event)'></div>
+''';
+    _addHtmlSource(code);
+    await _resolveSingleTemplate(dartSource);
+    assertErrorInCodeAtPosition(
+        AngularWarningCode.EVENT_REDUCTION_NOT_ALLOWED, code, '.whatever');
+  }
+
+  // ignore: non_constant_identifier_names
   Future test_expression_nativeEventBindingOnComponent() async {
     _addDartSource(r'''
 import 'dart:html';
diff --git a/angular_analyzer_plugin/test/test_all.dart b/angular_analyzer_plugin/test/test_all.dart
index 7c0c06c..de05fc3 100644
--- a/angular_analyzer_plugin/test/test_all.dart
+++ b/angular_analyzer_plugin/test/test_all.dart
@@ -1,6 +1,7 @@
 import 'package:test_reflective_loader/test_reflective_loader.dart';
 
 import 'angular_driver_test.dart' as angular_driver_test;
+import 'ast.dart' as ast_test;
 import 'completion_contributor_test.dart' as completion_contributor_test;
 import 'file_tracker_test.dart' as file_tracker_test;
 import 'navigation_test.dart' as navigation_test;
@@ -19,6 +20,7 @@
     resolver_test.main();
     selector_test.main();
     angular_driver_test.main();
+    ast_test.main();
     offsetting_constant_value_visitor_test.main();
     navigation_test.main();
     occurrences_test.main();