Remove facade classes, find unexported terms in its own visitor.
Reused `AngularSubsetVisitor`, which seems like a perfect place for
this. A bit unnerving with the number of edge cases I have to account
for in this solution, but I think I covered everything.
Added a somewhat ominous CHANGELOG comment for that reason.
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 658e73d..05dbb5e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,6 @@
## Unpublished
+- Refactored how `exports` are handled in a fairly major way.
- Typecheck the results and input of pipe expressions and the existence of a
matching pipe. Optional arguments are not yet typechecked.
- Add typechecking support for `[attr.foo.if]`, and ensure that a corresponding
diff --git a/angular_analyzer_plugin/lib/errors.dart b/angular_analyzer_plugin/lib/errors.dart
index 50e964e..1626f97 100644
--- a/angular_analyzer_plugin/lib/errors.dart
+++ b/angular_analyzer_plugin/lib/errors.dart
@@ -56,6 +56,7 @@
AngularWarningCode.MATCHED_LET_BINDING_HAS_WRONG_TYPE,
AngularWarningCode.EXPORTS_MUST_BE_PLAIN_IDENTIFIERS,
AngularWarningCode.DUPLICATE_EXPORT,
+ AngularWarningCode.IDENTIFIER_NOT_EXPORTED,
AngularWarningCode.COMPONENTS_CANT_EXPORT_THEMSELVES,
AngularWarningCode.PIPE_SINGLE_NAME_REQUIRED,
AngularWarningCode.TYPE_IS_NOT_A_PIPE,
@@ -450,6 +451,14 @@
static const DUPLICATE_EXPORT = const AngularWarningCode(
'DUPLICATE_EXPORT', 'Duplicate export of identifier {0}');
+ /// An error code indicating that an identifier was used in a template, but
+ /// not exported in the component.
+ static const IDENTIFIER_NOT_EXPORTED = const AngularWarningCode(
+ 'IDENTIFIER_NOT_EXPORTED',
+ 'Identifier {0} was not exported by the component and therefore cannot be'
+ ' used in its template. Add it to the exports property on the component'
+ ' definition to use it here.');
+
/// An error code indicating component Foo exports Foo, which is unnecessary
static const COMPONENTS_CANT_EXPORT_THEMSELVES = const AngularWarningCode(
'COMPONENTS_CANT_EXPORT_THEMSELVES',
diff --git a/angular_analyzer_plugin/lib/src/facade/exports_compilation_unit_element.dart b/angular_analyzer_plugin/lib/src/facade/exports_compilation_unit_element.dart
deleted file mode 100644
index 752e6ff..0000000
--- a/angular_analyzer_plugin/lib/src/facade/exports_compilation_unit_element.dart
+++ /dev/null
@@ -1,72 +0,0 @@
-import 'package:analyzer/dart/element/element.dart';
-import 'package:analyzer/src/dart/element/wrapped.dart';
-import 'package:angular_analyzer_plugin/src/model.dart';
-
-/// A facade for a [CompilationUnitElement] which consists only of a component's
-/// exports, and the component itself.
-class ExportsLimitedCompilationUnitFacade
- extends WrappedCompilationUnitElement {
- final Component _component;
- LibraryElement libraryFacade;
-
- ExportsLimitedCompilationUnitFacade(
- CompilationUnitElement wrappedUnit, this._component,
- {this.libraryFacade})
- : super(wrappedUnit);
-
- @override
- List<PropertyAccessorElement> get accessors =>
- new List<PropertyAccessorElement>.from(_component.exports
- .where(_fromThisUnit)
- .map((export) => export.element)
- .where((element) => element is PropertyAccessorElement));
-
- @override
- LibraryElement get enclosingElement => libraryFacade;
-
- @override
- List<ClassElement> get enums => new List<ClassElement>.from(_component.exports
- .where(_fromThisUnit)
- .map((export) => export.element)
- .where((element) => element is ClassElement && element.isEnum));
-
- @override
- List<FunctionElement> get functions =>
- new List<FunctionElement>.from(_component.exports
- .where(_fromThisUnit)
- .map((export) => export.element)
- .where((element) => element is FunctionElement));
-
- @override
- List<FunctionTypeAliasElement> get functionTypeAliases => [];
-
- @override
- bool get hasJS => false;
-
- @override
- bool get isJS => false;
-
- @override
- LibraryElement get library => libraryFacade;
-
- @override
- List<TopLevelVariableElement> get topLevelVariables => [];
-
- @override
- List<ClassElement> get types => new List<ClassElement>.from(_component.exports
- .where(_fromThisUnit)
- .map((export) => export.element)
- .where((element) => element is ClassElement))
- ..add(_component.classElement);
-
- @override
- ClassElement getEnum(String name) =>
- enums.firstWhere((_enum) => _enum.name == name, orElse: () => null);
-
- @override
- ClassElement getType(String className) =>
- types.firstWhere((type) => type.name == name, orElse: () => null);
-
- // CompilationUnitFacade's are not used for imports, which have prefixes
- bool _fromThisUnit(ExportedIdentifier export) => export.prefix == '';
-}
diff --git a/angular_analyzer_plugin/lib/src/facade/exports_import_element.dart b/angular_analyzer_plugin/lib/src/facade/exports_import_element.dart
deleted file mode 100644
index b75876f..0000000
--- a/angular_analyzer_plugin/lib/src/facade/exports_import_element.dart
+++ /dev/null
@@ -1,33 +0,0 @@
-import 'package:analyzer/dart/element/element.dart';
-import 'package:analyzer/src/dart/element/wrapped.dart';
-import 'package:angular_analyzer_plugin/src/facade/exports_library_element.dart';
-import 'package:angular_analyzer_plugin/src/model.dart';
-
-/// A facade for a [ImportElement] which consists only of a component's
-/// exports
-class ExportsImportElementFacade extends WrappedImportElement {
- final Component _component;
- LibraryElement libraryFacade;
-
- ExportsImportElementFacade(ImportElement wrappedImport, this._component,
- {this.libraryFacade})
- : super(wrappedImport);
-
- @override
- LibraryElement get enclosingElement => libraryFacade;
-
- @override
- bool get hasJS => false;
-
- @override
- LibraryElement get importedLibrary => wrappedImport.importedLibrary == null
- ? null
- : new ExportsLibraryFacade(wrappedImport.importedLibrary, _component,
- prefix: prefix?.name);
-
- @override
- bool get isJS => false;
-
- @override
- LibraryElement get library => libraryFacade;
-}
diff --git a/angular_analyzer_plugin/lib/src/facade/exports_library_element.dart b/angular_analyzer_plugin/lib/src/facade/exports_library_element.dart
deleted file mode 100644
index b682329..0000000
--- a/angular_analyzer_plugin/lib/src/facade/exports_library_element.dart
+++ /dev/null
@@ -1,108 +0,0 @@
-import 'package:analyzer/dart/element/element.dart';
-import 'package:analyzer/src/dart/element/wrapped.dart';
-import 'package:analyzer/src/generated/resolver.dart';
-import 'package:angular_analyzer_plugin/src/facade/exports_compilation_unit_element.dart';
-import 'package:angular_analyzer_plugin/src/facade/exports_import_element.dart';
-import 'package:angular_analyzer_plugin/src/model.dart';
-
-/// A facade for a [Library] which consists only of a components' exports, and
-/// the component itself.
-class ExportsLibraryFacade extends WrappedLibraryElement {
- final ExportsLimitedCompilationUnitFacade _definingUnit;
- final Component _owningComponent;
-
- factory ExportsLibraryFacade(
- LibraryElement wrappedLib, Component owningComponent,
- {String prefix}) {
- if (prefix != null) {
- return new _PrefixedExportsLibraryFacade(
- wrappedLib, owningComponent, prefix);
- }
- return new ExportsLibraryFacade._(wrappedLib, owningComponent);
- }
-
- ExportsLibraryFacade._(LibraryElement wrappedLib, this._owningComponent)
- : _definingUnit = new ExportsLimitedCompilationUnitFacade(
- wrappedLib.definingCompilationUnit, _owningComponent),
- super(wrappedLib) {
- _definingUnit.libraryFacade = this;
- }
-
- @override
- CompilationUnitElement get definingCompilationUnit => _definingUnit;
-
- @override
- bool get hasJS => false;
-
- @override
- List<ImportElement> get imports => wrappedLib.imports
- .map((import) => new ExportsImportElementFacade(import, _owningComponent,
- libraryFacade: this))
- .toList();
-
- @override
- bool get isDartAsync => false;
-
- @override
- bool get isDartCore => false;
-
- @override
- bool get isInSdk => false;
-
- @override
- bool get isJS => false;
-
- @override
- LibraryElement get library => this;
-
- @override
- List<LibraryElement> get libraryCycle => wrappedLib.libraryCycle
- .map((lib) => new ExportsLibraryFacade(lib, _owningComponent))
- .toList();
-
- @override
- List<CompilationUnitElement> get parts => wrappedLib.parts
- .map((part) => new ExportsLimitedCompilationUnitFacade(
- part, _owningComponent,
- libraryFacade: this))
- .toList();
-
- @override
- List<PrefixElement> get prefixes => wrappedLib.prefixes
- .where((prefix) => _owningComponent.exports
- .any((export) => export.prefix == prefix.name))
- .toList();
-
- @override
- List<CompilationUnitElement> get units => wrappedLib.units
- .map((unit) => new ExportsLimitedCompilationUnitFacade(
- unit, _owningComponent,
- libraryFacade: this))
- .toList();
-
- @override
- List<ImportElement> getImportsWithPrefix(PrefixElement prefix) => wrappedLib
- .getImportsWithPrefix(prefix)
- .map((lib) => new ExportsImportElementFacade(lib, _owningComponent,
- libraryFacade: this))
- .toList();
-
- @override
- ClassElement getType(String className) => wrappedLib.getType(className);
-}
-
-class _PrefixedExportsLibraryFacade extends ExportsLibraryFacade {
- final String _prefix;
- _PrefixedExportsLibraryFacade(
- LibraryElement wrappedLib, Component owningComponent, this._prefix)
- : super._(wrappedLib, owningComponent);
-
- @override
- Namespace get exportNamespace {
- final map = <String, Element>{};
- _owningComponent.exports
- .where((export) => export.prefix == _prefix)
- .forEach((export) => map[export.identifier] = export.element);
- return new Namespace(map);
- }
-}
diff --git a/angular_analyzer_plugin/lib/src/resolver.dart b/angular_analyzer_plugin/lib/src/resolver.dart
index 3d05268..86df668 100644
--- a/angular_analyzer_plugin/lib/src/resolver.dart
+++ b/angular_analyzer_plugin/lib/src/resolver.dart
@@ -15,7 +15,6 @@
import 'package:analyzer/src/generated/source.dart';
import 'package:angular_analyzer_plugin/ast.dart';
import 'package:angular_analyzer_plugin/errors.dart';
-import 'package:angular_analyzer_plugin/src/facade/exports_library_element.dart';
import 'package:angular_analyzer_plugin/src/model.dart';
import 'package:angular_analyzer_plugin/src/options.dart';
import 'package:angular_analyzer_plugin/src/selector.dart';
@@ -199,15 +198,19 @@
}
}
-/// Find nodes which are not supported in angular, such as compound assignment
-/// and function expressions etc.
+/// Find nodes which are not supported in angular (such as compound assignment
+/// and function expressions etc.), as well as terms used in the template that
+/// weren't exported by the component.
class AngularSubsetVisitor extends RecursiveAstVisitor<Object> {
final bool acceptAssignment;
+ final Component owningComponent;
final ErrorReporter errorReporter;
AngularSubsetVisitor(
- {@required this.errorReporter, @required this.acceptAssignment});
+ {@required this.errorReporter,
+ @required this.owningComponent,
+ @required this.acceptAssignment});
@override
void visitAsExpression(AsExpression exp) {
@@ -217,7 +220,9 @@
_reportDisallowedExpression(exp, "As expression", visitChildren: false);
}
- super.visitAsExpression(exp);
+ // Don't visit the TypeName or it may suggest exporting it, which is not
+ // possible.
+ exp.expression.accept(this);
}
@override
@@ -251,18 +256,112 @@
void visitFunctionExpression(FunctionExpression exp) =>
_reportDisallowedExpression(exp, "Anonymous functions");
- @override
- void visitInstanceCreationExpression(InstanceCreationExpression exp) =>
- _reportDisallowedExpression(exp, "Usage of new");
+ /// Only allow access to:
+ /// * current class members
+ /// * inherited class members
+ /// * methods
+ /// * angular references (ie `<h1 #ref id="foo"></h1> {{h1.id}}`)
+ /// * exported members
+ ///
+ /// Flag the rest and give the hint that they should be exported.
+ void visitIdentifier(Identifier id) {
+ final element = id.staticElement;
+ if (id is PrefixedIdentifier && id.prefix.staticElement is! PrefixElement) {
+ // Static methods, enums, etc. Check the LHS.
+ visitIdentifier(id.prefix);
+ return;
+ }
+ if (id.parent is PropertyAccess &&
+ identical(id, (id.parent as PropertyAccess).propertyName)) {
+ // Accessors are always allowed.
+ return;
+ }
+ if (element is PrefixElement) {
+ // Prefixes can't be exported, and analyzer reports a warning for dangling
+ // prefixes.
+ return;
+ }
+ if (element is MethodElement) {
+ // All methods are OK, as in `x.y()`. It's only `x` that may be hidden.
+ return;
+ }
+ if (element is ClassElement && element == owningComponent.classElement) {
+ // Static method calls on the current class are allowed
+ return;
+ }
+ if (element is DynamicElementImpl) {
+ // Usually indicates a resolution error, so don't double report it.
+ return;
+ }
+ if (element == null) {
+ // Also usually indicates an error, don't double report.
+ return;
+ }
+ if (element is LocalVariableElement) {
+ // `$event` variables, `ngFor` variables, these are OK.
+ return;
+ }
+ if (element is ParameterElement) {
+ // Named parameters: not allowed, but flagged in [visitNamedExpression].
+ return;
+ }
+ if (element is AngularElement) {
+ // Variables local to the template
+ return;
+ }
+ if ((element is PropertyInducingElement ||
+ element is PropertyAccessorElement) &&
+ (owningComponent.classElement.lookUpGetter(id.name, null) != null ||
+ owningComponent.classElement.lookUpSetter(id.name, null) != null)) {
+ // Part of the component interface.
+ return;
+ }
+
+ if (id is PrefixedIdentifier) {
+ if (owningComponent.exports.any((export) =>
+ export.prefix == id.prefix.name && id.name == export.identifier)) {
+ // Correct reference to exported prefix identifier
+ return;
+ }
+ } else {
+ if (owningComponent.exports.any(
+ (export) => export.prefix == null && id.name == export.identifier)) {
+ // Correct reference to exported simple identifier
+ return;
+ }
+ }
+
+ errorReporter.reportErrorForNode(
+ AngularWarningCode.IDENTIFIER_NOT_EXPORTED, id, [id]);
+ }
@override
- void visitIsExpression(IsExpression exp) =>
- _reportDisallowedExpression(exp, "Is expression");
+ void visitInstanceCreationExpression(InstanceCreationExpression exp) {
+ _reportDisallowedExpression(exp, "Usage of new", visitChildren: false);
+ // Don't visit the TypeName or it may suggest exporting it, which is not
+ // possible.
+
+ exp.argumentList.accept(this);
+ }
+
+ @override
+ void visitIsExpression(IsExpression exp) {
+ _reportDisallowedExpression(exp, "Is expression", visitChildren: false);
+ // Don't visit the TypeName or it may suggest exporting it, which is not
+ // possible.
+
+ exp.expression.accept(this);
+ }
@override
void visitListLiteral(ListLiteral list) {
if (list.typeArguments != null) {
- _reportDisallowedExpression(list, "Typed list literals");
+ _reportDisallowedExpression(list, "Typed list literals",
+ visitChildren: false);
+ // Don't visit the TypeName or it may suggest exporting it, which is not
+ // possible.e.
+
+ list.elements.accept(this);
} else {
super.visitListLiteral(list);
}
@@ -271,7 +370,12 @@
@override
void visitMapLiteral(MapLiteral map) {
if (map.typeArguments != null) {
- _reportDisallowedExpression(map, "Typed map literals");
+ _reportDisallowedExpression(map, "Typed map literals",
+ visitChildren: false);
+ // Don't visit the TypeName or it may suggest exporting it, which is not
+ // possible.e.
+
+ map.entries.accept(this);
} else {
super.visitMapLiteral(map);
}
@@ -287,6 +391,9 @@
}
@override
+ void visitPrefixedIdentifier(PrefixedIdentifier id) => visitIdentifier(id);
+
+ @override
void visitPrefixExpression(PrefixExpression exp) {
if (exp.operator.type != TokenType.MINUS) {
_reportDisallowedExpression(exp, exp.operator.lexeme);
@@ -294,6 +401,9 @@
}
@override
+ void visitSimpleIdentifier(SimpleIdentifier id) => visitIdentifier(id);
+
+ @override
void visitSymbolLiteral(SymbolLiteral exp) =>
_reportDisallowedExpression(exp, "Symbol literal");
@@ -1538,8 +1648,7 @@
/// Resolve the given [AstNode] ([expression] or [statement]) and report errors.
void _resolveDartAstNode(AstNode astNode, bool acceptAssignment) {
final classElement = view.classElement;
- final library =
- new ExportsLibraryFacade(classElement.library, view.component);
+ final library = classElement.library;
{
final visitor = new TypeResolverVisitor(
library, view.source, typeProvider, errorListener);
@@ -1568,7 +1677,9 @@
astNode.accept(verifier);
// Check for concepts illegal to templates (for instance function literals).
final angularSubsetChecker = new AngularSubsetVisitor(
- errorReporter: errorReporter, acceptAssignment: acceptAssignment);
+ errorReporter: errorReporter,
+ acceptAssignment: acceptAssignment,
+ owningComponent: view.component);
astNode.accept(angularSubsetChecker);
}
diff --git a/angular_analyzer_plugin/test/resolver_test.dart b/angular_analyzer_plugin/test/resolver_test.dart
index 8122080..c6fa8ba 100644
--- a/angular_analyzer_plugin/test/resolver_test.dart
+++ b/angular_analyzer_plugin/test/resolver_test.dart
@@ -3569,9 +3569,18 @@
class TestPanel {
static void componentStatic() {
}
+ local();
+ int get getter => null;
+ set setter(int x) => null;
+ int field = null;
}
''');
final code = r'''
+methods/getters/setters on current class ok:
+{{local()}}
+{{getter}}
+{{field}}
+<div (click)="setter = null"></div>
static on current class ok:
{{TestPanel.componentStatic()}}
exports ok:
@@ -3586,7 +3595,7 @@
''';
_addHtmlSource(code);
await _resolveSingleTemplate(dartSource);
- expect(ranges, hasLength(18));
+ expect(ranges, hasLength(23));
_assertElement('TestPanel').dart.at('TestPanel {');
_assertElement('componentStatic').dart.method.at('componentStatic() {');
_assertElement('myAccessor').dart.getter.at('myAccessor = 1');
@@ -3726,20 +3735,19 @@
''';
_addHtmlSource(code);
await _resolveSingleTemplate(dartSource);
- expect(ranges, hasLength(0));
errorListener.assertErrorsWithCodes([
- StaticWarningCode.UNDEFINED_IDENTIFIER,
- StaticWarningCode.UNDEFINED_IDENTIFIER,
- StaticWarningCode.UNDEFINED_IDENTIFIER,
- StaticTypeWarningCode.UNDEFINED_METHOD,
- StaticWarningCode.UNDEFINED_IDENTIFIER,
- StaticWarningCode.UNDEFINED_IDENTIFIER,
- StaticWarningCode.UNDEFINED_IDENTIFIER,
- StaticWarningCode.UNDEFINED_IDENTIFIER,
- StaticWarningCode.UNDEFINED_IDENTIFIER,
- StaticWarningCode.UNDEFINED_IDENTIFIER,
- StaticWarningCode.UNDEFINED_IDENTIFIER,
- StaticWarningCode.UNDEFINED_IDENTIFIER,
+ AngularWarningCode.IDENTIFIER_NOT_EXPORTED,
+ AngularWarningCode.IDENTIFIER_NOT_EXPORTED,
+ AngularWarningCode.IDENTIFIER_NOT_EXPORTED,
+ AngularWarningCode.IDENTIFIER_NOT_EXPORTED,
+ AngularWarningCode.IDENTIFIER_NOT_EXPORTED,
+ AngularWarningCode.IDENTIFIER_NOT_EXPORTED,
+ AngularWarningCode.IDENTIFIER_NOT_EXPORTED,
+ AngularWarningCode.IDENTIFIER_NOT_EXPORTED,
+ AngularWarningCode.IDENTIFIER_NOT_EXPORTED,
+ AngularWarningCode.IDENTIFIER_NOT_EXPORTED,
+ AngularWarningCode.IDENTIFIER_NOT_EXPORTED,
+ AngularWarningCode.IDENTIFIER_NOT_EXPORTED,
]);
}