feat(dart): Add a dev-mode check for undeclared lifecycle interfaces

Add a check in ReflectionCapabilities#interfaces which determines if
the passed-in type implements a Lifecycle Interface but does not declare
that it does so.

See https://goo.gl/b07Kii for details.

Closes #6849
This commit is contained in:
Tim Blasi 2016-03-29 10:55:06 -07:00
parent 8430927e6b
commit 1c20a62611
2 changed files with 130 additions and 3 deletions

View File

@ -1,9 +1,12 @@
library reflection.reflection_capabilities;
import 'package:angular2/src/facade/lang.dart';
import 'types.dart';
import 'dart:mirrors';
import 'package:angular2/src/core/linker/interfaces.dart';
import 'package:angular2/src/facade/lang.dart';
import 'platform_reflection_capabilities.dart';
import 'types.dart';
var DOT_REGEX = new RegExp('\\.');
@ -272,7 +275,9 @@ class ReflectionCapabilities implements PlatformReflectionCapabilities {
}
List interfaces(type) {
return _interfacesFromMirror(reflectType(type));
final clazz = reflectType(type);
_assertDeclaresLifecycleHooks(clazz);
return _interfacesFromMirror(clazz);
}
List _interfacesFromMirror(classMirror) {
@ -324,3 +329,103 @@ class ReflectionCapabilities implements PlatformReflectionCapabilities {
return '${(reflectClass(type).owner as LibraryMirror).uri}';
}
}
final _lifecycleHookMirrors = <ClassMirror>[
reflectType(AfterContentChecked),
reflectType(AfterContentInit),
reflectType(AfterViewChecked),
reflectType(AfterViewInit),
reflectType(DoCheck),
reflectType(OnChanges),
reflectType(OnDestroy),
reflectType(OnInit),
];
/// Checks whether [clazz] implements lifecycle ifaces without declaring them.
///
/// Due to Dart implementation details, lifecycle hooks are only called when a
/// class explicitly declares that it implements the associated interface.
/// See https://goo.gl/b07Kii for details.
void _assertDeclaresLifecycleHooks(ClassMirror clazz) {
final missingDeclarations = <ClassMirror>[];
for (var iface in _lifecycleHookMirrors) {
if (!_checkDeclares(clazz, iface: iface) &&
_checkImplements(clazz, iface: iface)) {
missingDeclarations.add(iface);
}
}
if (missingDeclarations.isNotEmpty) {
throw new MissingInterfaceError(clazz, missingDeclarations);
}
}
/// Returns whether [clazz] declares that it implements [iface].
///
/// Returns `false` if [clazz] implements [iface] but does not declare it.
/// Returns `false` if [clazz]'s superclass declares that it
/// implements [iface].
bool _checkDeclares(ClassMirror clazz, {ClassMirror iface: null}) {
if (iface == null) {
throw new ArgumentError.notNull('iface');
}
return clazz.superinterfaces.contains(iface);
}
/// Returns whether [clazz] implements [iface].
///
/// Returns `true` if [clazz] implements [iface] and does not declare it.
/// Returns `true` if [clazz]'s superclass implements [iface].
///
/// This is an approximation of a JavaScript feature check:
/// ```js
/// var matches = true;
/// for (var prop in iface) {
/// if (iface.hasOwnProperty(prop)) {
/// matches = matches && clazz.hasOwnProperty(prop);
/// }
/// }
/// return matches;
/// ```
bool _checkImplements(ClassMirror clazz, {ClassMirror iface: null}) {
if (iface == null) {
throw new ArgumentError.notNull('iface');
}
var matches = true;
iface.declarations.forEach((symbol, declarationMirror) {
if (!matches) return;
if (declarationMirror.isConstructor || declarationMirror.isPrivate) return;
matches = clazz.declarations.keys.contains(symbol);
});
if (!matches && clazz.superclass != null) {
matches = _checkImplements(clazz.superclass, iface: iface);
}
if (!matches && clazz.mixin != clazz) {
matches = _checkImplements(clazz.mixin, iface: iface);
}
return matches;
}
/// Error thrown when a class implements a lifecycle iface it does not declare.
class MissingInterfaceError extends Error {
final ClassMirror clazz;
final List<ClassMirror> missingDeclarations;
MissingInterfaceError(this.clazz, this.missingDeclarations);
@override
String toString() {
final buf = new StringBuffer();
buf.write('${clazz.simpleName} implements ');
if (missingDeclarations.length == 1) {
buf.write('an interface but does not declare it: ');
} else {
buf.write('interfaces but does not declare them: ');
}
buf.write(
missingDeclarations.map((d) => d.simpleName.toString()).join(', '));
buf.write('. See https://goo.gl/b07Kii for more info.');
return buf.toString();
}
}

View File

@ -7,6 +7,7 @@ import {
beforeEach,
browserDetection
} from 'angular2/testing_internal';
import {OnInit} from 'angular2/core';
import {Reflector, ReflectionInfo} from 'angular2/src/core/reflection/reflection';
import {ReflectionCapabilities} from 'angular2/src/core/reflection/reflection_capabilities';
import {
@ -65,6 +66,19 @@ class SuperClassImplementingInterface implements Interface2 {}
class ClassImplementingInterface extends SuperClassImplementingInterface implements Interface {}
// Classes used to test our runtime check for classes that implement lifecycle interfaces but do not
// declare them.
// See https://github.com/angular/angular/pull/6879 and https://goo.gl/b07Kii for details.
class ClassDoesNotDeclareOnInit {
ngOnInit() {}
}
class SuperClassImplementingOnInit implements OnInit {
ngOnInit() {}
}
class SubClassDoesNotDeclareOnInit extends SuperClassImplementingOnInit {}
export function main() {
describe('Reflector', () => {
var reflector;
@ -214,6 +228,14 @@ export function main() {
var p = reflector.interfaces(ClassWithDecorators);
expect(p).toEqual([]);
});
it("should throw for undeclared lifecycle interfaces",
() => { expect(() => reflector.interfaces(ClassDoesNotDeclareOnInit)).toThrowError(); });
it("should throw for class inheriting a lifecycle impl and not declaring the interface",
() => {
expect(() => reflector.interfaces(SubClassDoesNotDeclareOnInit)).toThrowError();
});
});
}