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:
parent
8430927e6b
commit
1c20a62611
|
@ -1,9 +1,12 @@
|
||||||
library reflection.reflection_capabilities;
|
library reflection.reflection_capabilities;
|
||||||
|
|
||||||
import 'package:angular2/src/facade/lang.dart';
|
|
||||||
import 'types.dart';
|
|
||||||
import 'dart:mirrors';
|
import 'dart:mirrors';
|
||||||
|
|
||||||
|
import 'package:angular2/src/core/linker/interfaces.dart';
|
||||||
|
import 'package:angular2/src/facade/lang.dart';
|
||||||
|
|
||||||
import 'platform_reflection_capabilities.dart';
|
import 'platform_reflection_capabilities.dart';
|
||||||
|
import 'types.dart';
|
||||||
|
|
||||||
var DOT_REGEX = new RegExp('\\.');
|
var DOT_REGEX = new RegExp('\\.');
|
||||||
|
|
||||||
|
@ -272,7 +275,9 @@ class ReflectionCapabilities implements PlatformReflectionCapabilities {
|
||||||
}
|
}
|
||||||
|
|
||||||
List interfaces(type) {
|
List interfaces(type) {
|
||||||
return _interfacesFromMirror(reflectType(type));
|
final clazz = reflectType(type);
|
||||||
|
_assertDeclaresLifecycleHooks(clazz);
|
||||||
|
return _interfacesFromMirror(clazz);
|
||||||
}
|
}
|
||||||
|
|
||||||
List _interfacesFromMirror(classMirror) {
|
List _interfacesFromMirror(classMirror) {
|
||||||
|
@ -324,3 +329,103 @@ class ReflectionCapabilities implements PlatformReflectionCapabilities {
|
||||||
return '${(reflectClass(type).owner as LibraryMirror).uri}';
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -7,6 +7,7 @@ import {
|
||||||
beforeEach,
|
beforeEach,
|
||||||
browserDetection
|
browserDetection
|
||||||
} from 'angular2/testing_internal';
|
} from 'angular2/testing_internal';
|
||||||
|
import {OnInit} from 'angular2/core';
|
||||||
import {Reflector, ReflectionInfo} from 'angular2/src/core/reflection/reflection';
|
import {Reflector, ReflectionInfo} from 'angular2/src/core/reflection/reflection';
|
||||||
import {ReflectionCapabilities} from 'angular2/src/core/reflection/reflection_capabilities';
|
import {ReflectionCapabilities} from 'angular2/src/core/reflection/reflection_capabilities';
|
||||||
import {
|
import {
|
||||||
|
@ -65,6 +66,19 @@ class SuperClassImplementingInterface implements Interface2 {}
|
||||||
|
|
||||||
class ClassImplementingInterface extends SuperClassImplementingInterface implements Interface {}
|
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() {
|
export function main() {
|
||||||
describe('Reflector', () => {
|
describe('Reflector', () => {
|
||||||
var reflector;
|
var reflector;
|
||||||
|
@ -214,6 +228,14 @@ export function main() {
|
||||||
var p = reflector.interfaces(ClassWithDecorators);
|
var p = reflector.interfaces(ClassWithDecorators);
|
||||||
expect(p).toEqual([]);
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue