From 0b1bb172c9dfdf00dec2e02cb08933a4f9006f18 Mon Sep 17 00:00:00 2001 From: keertip Date: Mon, 4 May 2015 09:20:27 -0700 Subject: [PATCH] feat(dart/analysis): Build DirectiveMetadata for LibrarySpecificUnit initial commit for the dart analyzer task --- .../lib/plugin.dart | 7 +- .../analyzer_plugin/lib/src/tasks.dart | 112 +++++++ modules_dart/analyzer_plugin/lib/tasks.dart | 9 + .../pubspec.yaml | 6 +- .../analyzer_plugin/test/mock_sdk.dart | 311 ++++++++++++++++++ .../test/tasks_test.server.spec.dart | 205 ++++++++++++ 6 files changed, 647 insertions(+), 3 deletions(-) rename modules_dart/{analysis_plugin => analyzer_plugin}/lib/plugin.dart (70%) create mode 100644 modules_dart/analyzer_plugin/lib/src/tasks.dart create mode 100644 modules_dart/analyzer_plugin/lib/tasks.dart rename modules_dart/{analysis_plugin => analyzer_plugin}/pubspec.yaml (66%) create mode 100644 modules_dart/analyzer_plugin/test/mock_sdk.dart create mode 100644 modules_dart/analyzer_plugin/test/tasks_test.server.spec.dart diff --git a/modules_dart/analysis_plugin/lib/plugin.dart b/modules_dart/analyzer_plugin/lib/plugin.dart similarity index 70% rename from modules_dart/analysis_plugin/lib/plugin.dart rename to modules_dart/analyzer_plugin/lib/plugin.dart index ddf9b0e9ca..29f3118199 100644 --- a/modules_dart/analysis_plugin/lib/plugin.dart +++ b/modules_dart/analyzer_plugin/lib/plugin.dart @@ -1,12 +1,14 @@ library angular2.src.analysis.analyzer_plugin; import 'package:analyzer/plugin/plugin.dart'; +import 'package:analyzer/plugin/task.dart'; +import 'src/tasks.dart'; /// Contribute a plugin to the dart analyzer for analysis of /// Angular 2 dart code. class AngularAnalyzerPlugin implements Plugin { - /// the unique indetifier for this plugin + /// The unique identifier for this plugin. static const String UNIQUE_IDENTIFIER = 'angular2.analysis'; @override @@ -17,6 +19,7 @@ class AngularAnalyzerPlugin implements Plugin { @override void registerExtensions(RegisterExtension registerExtension) { - // TODO(keerti): register extension for analysis + String taskId = TASK_EXTENSION_POINT_ID; + registerExtension(taskId, BuildUnitDirectivesTask.DESCRIPTOR); } } diff --git a/modules_dart/analyzer_plugin/lib/src/tasks.dart b/modules_dart/analyzer_plugin/lib/src/tasks.dart new file mode 100644 index 0000000000..08f00d1938 --- /dev/null +++ b/modules_dart/analyzer_plugin/lib/src/tasks.dart @@ -0,0 +1,112 @@ +library angular2.src.analysis.analyzer_plugin.src.tasks; + +import 'package:analyzer/src/generated/ast.dart' hide Directive; +import 'package:analyzer/src/generated/element.dart'; +import 'package:analyzer/src/generated/engine.dart'; +import 'package:analyzer/src/task/general.dart'; +import 'package:analyzer/task/dart.dart'; +import 'package:analyzer/task/model.dart'; +import 'package:angular2/src/core/annotations/annotations.dart'; +import 'package:angular2/src/render/api.dart'; + +/// The [DirectiveMetadata]s of a [LibrarySpecificUnit]. +final ListResultDescriptor DIRECTIVES = + new ListResultDescriptor('ANGULAR2_DIRECTIVES', null); + +/// A task that builds [DirectiveMetadata]s for directive classes. +class BuildUnitDirectivesTask extends SourceBasedAnalysisTask { + static const String UNIT_INPUT = 'UNIT_INPUT'; + + static final TaskDescriptor DESCRIPTOR = new TaskDescriptor( + 'BuildUnitDirectivesTask', createTask, buildInputs, + [DIRECTIVES]); + + BuildUnitDirectivesTask(AnalysisContext context, AnalysisTarget target) + : super(context, target); + + @override + TaskDescriptor get descriptor => DESCRIPTOR; + + @override + void internalPerform() { + CompilationUnit unit = getRequiredInput(UNIT_INPUT); + List metaList = []; + for (CompilationUnitMember unitMember in unit.declarations) { + if (unitMember is ClassDeclaration) { + for (Annotation annotationNode in unitMember.metadata) { + Directive directive = _createDirective(annotationNode); + if (directive != null) { + DirectiveMetadata meta = new DirectiveMetadata( + type: _getDirectiveType(directive), + selector: directive.selector); + metaList.add(meta); + } + } + } + } + outputs[DIRECTIVES] = metaList; + } + + /// Returns an Angular [Directive] that corresponds to the given [node]. + /// Returns `null` if not an Angular annotation. + Directive _createDirective(Annotation node) { + // TODO(scheglov) add support for all arguments + if (_isAngularAnnotation(node, 'Component')) { + String selector = _getNamedArgument(node, 'selector'); + return new Component(selector: selector); + } + if (_isAngularAnnotation(node, 'Directive')) { + String selector = _getNamedArgument(node, 'selector'); + return new Directive(selector: selector); + } + return null; + } + + int _getDirectiveType(Directive directive) { + if (directive is Component) { + return DirectiveMetadata.COMPONENT_TYPE; + } + return DirectiveMetadata.DIRECTIVE_TYPE; + } + + /// Returns the value of an argument with the given [name]. + /// Returns `null` if not found or cannot be evaluated statically. + Object _getNamedArgument(Annotation node, String name) { + if (node.arguments != null) { + List arguments = node.arguments.arguments; + for (Expression argument in arguments) { + if (argument is NamedExpression && + argument.name != null && + argument.name.label != null && + argument.name.label.name == name) { + Expression expression = argument.expression; + if (expression is SimpleStringLiteral) { + return expression.value; + } + } + } + } + return null; + } + + /// Returns `true` is the given [node] is resolved to a creation of an Angular + /// annotation class with the given [name]. + bool _isAngularAnnotation(Annotation node, String name) { + if (node.element is ConstructorElement) { + ClassElement clazz = node.element.enclosingElement; + return clazz.library.name == + 'angular2.src.core.annotations.annotations' && + clazz.name == name; + } + return null; + } + + static Map buildInputs(LibrarySpecificUnit target) { + return {UNIT_INPUT: RESOLVED_UNIT.of(target)}; + } + + static BuildUnitDirectivesTask createTask( + AnalysisContext context, LibrarySpecificUnit target) { + return new BuildUnitDirectivesTask(context, target); + } +} diff --git a/modules_dart/analyzer_plugin/lib/tasks.dart b/modules_dart/analyzer_plugin/lib/tasks.dart new file mode 100644 index 0000000000..a8447cb287 --- /dev/null +++ b/modules_dart/analyzer_plugin/lib/tasks.dart @@ -0,0 +1,9 @@ +library angular2.src.analysis.analyzer_plugin.tasks; + +import 'package:analyzer/src/generated/error.dart'; +import 'package:analyzer/task/model.dart'; + +/// The analysis errors associated with a target. +/// The value combines errors represented by multiple other results. +final CompositeResultDescriptor> HTML_ERRORS = + new CompositeResultDescriptor>('ANGULAR_HTML_ERRORS'); diff --git a/modules_dart/analysis_plugin/pubspec.yaml b/modules_dart/analyzer_plugin/pubspec.yaml similarity index 66% rename from modules_dart/analysis_plugin/pubspec.yaml rename to modules_dart/analyzer_plugin/pubspec.yaml index 4c29d5f029..56aa7ffe8e 100644 --- a/modules_dart/analysis_plugin/pubspec.yaml +++ b/modules_dart/analyzer_plugin/pubspec.yaml @@ -1,4 +1,4 @@ -name: angular2_analysis_plugin +name: angular2_analyzer_plugin version: 0.0.0 description: Dart analyzer plugin for Angular 2 environment: @@ -6,6 +6,10 @@ environment: dependencies: angular2: '0.0.0' analyzer: '^0.24.4' +dev_dependencies: + unittest: any + typed_mock: any + test_reflective_loader: any dependency_overrides: angular2: path: ../../dist/dart/angular2 diff --git a/modules_dart/analyzer_plugin/test/mock_sdk.dart b/modules_dart/analyzer_plugin/test/mock_sdk.dart new file mode 100644 index 0000000000..b8c95c993b --- /dev/null +++ b/modules_dart/analyzer_plugin/test/mock_sdk.dart @@ -0,0 +1,311 @@ +library test.src.mock_sdk; + +import 'package:analyzer/file_system/file_system.dart' as resource; +import 'package:analyzer/file_system/memory_file_system.dart' as resource; +import 'package:analyzer/src/generated/engine.dart'; +import 'package:analyzer/src/generated/sdk.dart'; +import 'package:analyzer/src/generated/source.dart'; + +class MockSdk implements DartSdk { + static const _MockSdkLibrary LIB_CORE = const _MockSdkLibrary('dart:core', + '/lib/core/core.dart', ''' +library dart.core; + +import 'dart:async'; + +class Object { + bool operator ==(other) => identical(this, other); + String toString() => 'a string'; + int get hashCode => 0; +} + +class Function {} +class StackTrace {} +class Symbol {} +class Type {} + +abstract class Comparable { + int compareTo(T other); +} + +abstract class String implements Comparable { + external factory String.fromCharCodes(Iterable charCodes, + [int start = 0, int end]); + bool get isEmpty => false; + bool get isNotEmpty => false; + int get length => 0; + String toUpperCase(); + List get codeUnits; +} + +class bool extends Object {} +abstract class num implements Comparable { + bool operator <(num other); + bool operator <=(num other); + bool operator >(num other); + bool operator >=(num other); + num operator +(num other); + num operator -(num other); + num operator *(num other); + num operator /(num other); + int toInt(); + num abs(); + int round(); +} +abstract class int extends num { + bool get isEven => false; + int operator -(); + external static int parse(String source, + { int radix, + int onError(String source) }); +} +class double extends num {} +class DateTime extends Object {} +class Null extends Object {} + +class Deprecated extends Object { + final String expires; + const Deprecated(this.expires); +} +const Object deprecated = const Deprecated("next release"); + +class Iterator { + bool moveNext(); + E get current; +} + +abstract class Iterable { + Iterator get iterator; + bool get isEmpty; +} + +abstract class List implements Iterable { + void add(E value); + E operator [](int index); + void operator []=(int index, E value); + Iterator get iterator => null; + void clear(); +} + +abstract class Map extends Object { + Iterable get keys; +} + +external bool identical(Object a, Object b); + +void print(Object object) {} + +class _Override { + const _Override(); +} +const Object override = const _Override(); +'''); + + static const _MockSdkLibrary LIB_ASYNC = const _MockSdkLibrary('dart:async', + '/lib/async/async.dart', ''' +library dart.async; + +import 'dart:math'; + +class Future { + factory Future.delayed(Duration duration, [T computation()]) => null; + factory Future.value([value]) => null; + static Future wait(List futures) => null; +} + +class Stream {} +abstract class StreamTransformer {} +'''); + + static const _MockSdkLibrary LIB_COLLECTION = const _MockSdkLibrary( + 'dart:collection', '/lib/collection/collection.dart', ''' +library dart.collection; + +abstract class HashMap implements Map {} +'''); + + static const _MockSdkLibrary LIB_CONVERT = const _MockSdkLibrary( + 'dart:convert', '/lib/convert/convert.dart', ''' +library dart.convert; + +import 'dart:async'; + +abstract class Converter implements StreamTransformer {} +class JsonDecoder extends Converter {} +'''); + + static const _MockSdkLibrary LIB_MATH = const _MockSdkLibrary('dart:math', + '/lib/math/math.dart', ''' +library dart.math; +const double E = 2.718281828459045; +const double PI = 3.1415926535897932; +const double LN10 = 2.302585092994046; +num min(num a, num b) => 0; +num max(num a, num b) => 0; +external double cos(num x); +external double sin(num x); +external double sqrt(num x); +class Random { + bool nextBool() => true; + double nextDouble() => 2.0; + int nextInt() => 1; +} +'''); + + static const _MockSdkLibrary LIB_HTML = const _MockSdkLibrary('dart:html', + '/lib/html/dartium/html_dartium.dart', ''' +library dart.html; +class HtmlElement {} +'''); + + static const List LIBRARIES = const [ + LIB_CORE, + LIB_ASYNC, + LIB_COLLECTION, + LIB_CONVERT, + LIB_MATH, + LIB_HTML, + ]; + + final resource.MemoryResourceProvider provider = + new resource.MemoryResourceProvider(); + + /** + * The [AnalysisContext] which is used for all of the sources. + */ + InternalAnalysisContext _analysisContext; + + MockSdk() { + LIBRARIES.forEach((_MockSdkLibrary library) { + provider.newFile(library.path, library.content); + }); + } + + @override + AnalysisContext get context { + if (_analysisContext == null) { + _analysisContext = new SdkAnalysisContext(); + SourceFactory factory = new SourceFactory([new DartUriResolver(this)]); + _analysisContext.sourceFactory = factory; + ChangeSet changeSet = new ChangeSet(); + for (String uri in uris) { + Source source = factory.forUri(uri); + changeSet.addedSource(source); + } + _analysisContext.applyChanges(changeSet); + } + return _analysisContext; + } + + @override + List get sdkLibraries => LIBRARIES; + + @override + String get sdkVersion => throw unimplemented; + + UnimplementedError get unimplemented => new UnimplementedError(); + + @override + List get uris { + List uris = []; + for (SdkLibrary library in LIBRARIES) { + uris.add(library.shortName); + } + return uris; + } + + @override + Source fromFileUri(Uri uri) { + String filePath = uri.path; + String libPath = '/lib'; + if (!filePath.startsWith("$libPath/")) { + return null; + } + for (SdkLibrary library in LIBRARIES) { + String libraryPath = library.path; + if (filePath.replaceAll('\\', '/') == libraryPath) { + try { + resource.File file = provider.getResource(uri.path); + Uri dartUri = Uri.parse(library.shortName); + return file.createSource(dartUri); + } catch (exception) { + return null; + } + } + if (filePath.startsWith("$libraryPath/")) { + String pathInLibrary = filePath.substring(libraryPath.length + 1); + String path = '${library.shortName}/${pathInLibrary}'; + try { + resource.File file = provider.getResource(uri.path); + Uri dartUri = new Uri(scheme: 'dart', path: path); + return file.createSource(dartUri); + } catch (exception) { + return null; + } + } + } + return null; + } + + @override + SdkLibrary getSdkLibrary(String dartUri) { + // getSdkLibrary() is only used to determine whether a library is internal + // to the SDK. The mock SDK doesn't have any internals, so it's safe to + // return null. + return null; + } + + @override + Source mapDartUri(String dartUri) { + const Map uriToPath = const { + "dart:core": "/lib/core/core.dart", + "dart:html": "/lib/html/dartium/html_dartium.dart", + "dart:async": "/lib/async/async.dart", + "dart:collection": "/lib/collection/collection.dart", + "dart:convert": "/lib/convert/convert.dart", + "dart:math": "/lib/math/math.dart" + }; + + String path = uriToPath[dartUri]; + if (path != null) { + resource.File file = provider.getResource(path); + Uri uri = new Uri(scheme: 'dart', path: dartUri.substring(5)); + return file.createSource(uri); + } + + // If we reach here then we tried to use a dartUri that's not in the + // table above. + return null; + } +} + +class _MockSdkLibrary implements SdkLibrary { + final String shortName; + final String path; + final String content; + + const _MockSdkLibrary(this.shortName, this.path, this.content); + + @override + String get category => throw unimplemented; + + @override + bool get isDart2JsLibrary => throw unimplemented; + + @override + bool get isDocumented => throw unimplemented; + + @override + bool get isImplementation => throw unimplemented; + + @override + bool get isInternal => throw unimplemented; + + @override + bool get isShared => throw unimplemented; + + @override + bool get isVmLibrary => throw unimplemented; + + UnimplementedError get unimplemented => new UnimplementedError(); +} diff --git a/modules_dart/analyzer_plugin/test/tasks_test.server.spec.dart b/modules_dart/analyzer_plugin/test/tasks_test.server.spec.dart new file mode 100644 index 0000000000..dc896f6712 --- /dev/null +++ b/modules_dart/analyzer_plugin/test/tasks_test.server.spec.dart @@ -0,0 +1,205 @@ +library angular2.src.analysis.analyzer_plugin.src.tasks_test; + +import 'package:analyzer/file_system/file_system.dart'; +import 'package:analyzer/file_system/memory_file_system.dart'; +import 'package:analyzer/src/context/cache.dart'; +import 'package:analyzer/src/generated/engine.dart' + show AnalysisOptionsImpl, TimestampedData; +import 'package:analyzer/src/generated/resolver.dart'; +import 'package:analyzer/src/generated/sdk.dart'; +import 'package:analyzer/src/generated/source.dart'; +import 'package:analyzer/src/task/dart.dart'; +import 'package:analyzer/src/task/driver.dart'; +import 'package:analyzer/src/task/general.dart'; +import 'package:analyzer/src/task/manager.dart'; +import 'package:analyzer/task/dart.dart'; +import 'package:analyzer/task/model.dart'; +import 'package:angular2/src/render/api.dart'; +import 'package:angular2_analyzer_plugin/src/tasks.dart'; +import 'package:test_reflective_loader/test_reflective_loader.dart'; +import 'package:typed_mock/typed_mock.dart'; +import 'package:unittest/unittest.dart'; + +import 'mock_sdk.dart'; + +main() { + groupSep = ' | '; + defineReflectiveTests(BuildUnitDirectivesTaskTest); +} + +@reflectiveTest +class BuildUnitDirectivesTaskTest extends _AbstractDartTaskTest { + MemoryResourceProvider resourceProvider = new MemoryResourceProvider(); + + void test_Component() { + _addAngularSources(); + Source source = _newSource('/test.dart', r''' +import '/angular2/annotations.dart'; + +@Component(selector: 'comp-a') +class ComponentA { +} + +@Component(selector: 'comp-b') +class ComponentB { +} +'''); + LibrarySpecificUnit target = new LibrarySpecificUnit(source, source); + _computeResult(target, DIRECTIVES); + expect(task, new isInstanceOf()); + // validate + List directives = outputs[DIRECTIVES]; + expect(directives, hasLength(2)); + expect(directives[0].selector, 'comp-a'); + expect(directives[1].selector, 'comp-b'); + } + + void test_Directive() { + _addAngularSources(); + Source source = _newSource('/test.dart', r''' +import '/angular2/annotations.dart'; + +@Directive(selector: 'deco-a') +class ComponentA { +} + +@Directive(selector: 'deco-b') +class ComponentB { +} +'''); + LibrarySpecificUnit target = new LibrarySpecificUnit(source, source); + _computeResult(target, DIRECTIVES); + expect(task, new isInstanceOf()); + // validate + List directives = outputs[DIRECTIVES]; + expect(directives, hasLength(2)); + expect(directives[0].selector, 'deco-a'); + expect(directives[1].selector, 'deco-b'); + } + + void _addAngularSources() { + _newSource('/angular2/annotations.dart', r''' +library angular2.src.core.annotations.annotations; + +abstract class Directive { + final String selector; + final dynamic properties; + final dynamic hostListeners; + final List lifecycle; + const Directive({selector, properties, hostListeners, lifecycle}) + : selector = selector, + properties = properties, + hostListeners = hostListeners, + lifecycle = lifecycle, + super(); +} + +class Component extends Directive { + final String changeDetection; + final List injectables; + const Component({selector, properties, events, hostListeners, + injectables, lifecycle, changeDetection: 'DEFAULT'}) + : changeDetection = changeDetection, + injectables = injectables, + super( + selector: selector, + properties: properties, + events: events, + hostListeners: hostListeners, + lifecycle: lifecycle); +} + +'''); + } +} + +class _AbstractDartTaskTest { + MemoryResourceProvider resourceProvider = new MemoryResourceProvider(); + Source emptySource; + + DartSdk sdk = new MockSdk(); + _MockContext context = new _MockContext(); + Map entryMap = {}; + + TaskManager taskManager = new TaskManager(); + AnalysisDriver analysisDriver; + + AnalysisTask task; + Map, dynamic> outputs; + + CacheEntry getCacheEntry(AnalysisTarget target) { + return entryMap.putIfAbsent(target, () => new CacheEntry()); + } + + void setUp() { + emptySource = _newSource('/test.dart'); + // prepare AnalysisContext + context.sourceFactory = new SourceFactory([ + new DartUriResolver(sdk), + new ResourceUriResolver(resourceProvider) + ]); + // prepare TaskManager + taskManager.addTaskDescriptor(GetContentTask.DESCRIPTOR); + // TODO(scheglov) extract into API + taskManager.addTaskDescriptor(ScanDartTask.DESCRIPTOR); + taskManager.addTaskDescriptor(ParseDartTask.DESCRIPTOR); + taskManager.addTaskDescriptor(BuildClassConstructorsTask.DESCRIPTOR); + taskManager.addTaskDescriptor(BuildCompilationUnitElementTask.DESCRIPTOR); + taskManager.addTaskDescriptor(BuildLibraryConstructorsTask.DESCRIPTOR); + taskManager.addTaskDescriptor(BuildLibraryElementTask.DESCRIPTOR); + taskManager.addTaskDescriptor(BuildPublicNamespaceTask.DESCRIPTOR); + taskManager.addTaskDescriptor(BuildDirectiveElementsTask.DESCRIPTOR); + taskManager.addTaskDescriptor(BuildSourceClosuresTask.DESCRIPTOR); + taskManager.addTaskDescriptor(BuildExportNamespaceTask.DESCRIPTOR); + taskManager.addTaskDescriptor(BuildEnumMemberElementsTask.DESCRIPTOR); + taskManager.addTaskDescriptor(BuildFunctionTypeAliasesTask.DESCRIPTOR); + taskManager.addTaskDescriptor(BuildTypeProviderTask.DESCRIPTOR); + taskManager.addTaskDescriptor(GatherUsedImportedElementsTask.DESCRIPTOR); + taskManager.addTaskDescriptor(GatherUsedLocalElementsTask.DESCRIPTOR); + taskManager.addTaskDescriptor(GenerateHintsTask.DESCRIPTOR); + taskManager.addTaskDescriptor(ResolveUnitTypeNamesTask.DESCRIPTOR); + taskManager.addTaskDescriptor(ResolveLibraryTypeNamesTask.DESCRIPTOR); + taskManager.addTaskDescriptor(ResolveReferencesTask.DESCRIPTOR); + taskManager.addTaskDescriptor(ResolveVariableReferencesTask.DESCRIPTOR); + taskManager.addTaskDescriptor(VerifyUnitTask.DESCRIPTOR); + // Angular specific tasks + taskManager.addTaskDescriptor(BuildUnitDirectivesTask.DESCRIPTOR); + // prepare AnalysisDriver + analysisDriver = new AnalysisDriver(taskManager, context); + } + + void _computeResult(AnalysisTarget target, ResultDescriptor result) { + task = analysisDriver.computeResult(target, result); + expect(task.caughtException, isNull); + outputs = task.outputs; + } + + Source _newSource(String path, [String content = '']) { + File file = resourceProvider.newFile(path, content); + return file.createSource(); + } +} + +class _MockContext extends TypedMock implements ExtendedAnalysisContext { + AnalysisOptionsImpl analysisOptions = new AnalysisOptionsImpl(); + SourceFactory sourceFactory; + TypeProvider typeProvider; + + Map entryMap = {}; + + String get name => '_MockContext'; + + bool exists(Source source) => source.exists(); + + @override + CacheEntry getCacheEntry(AnalysisTarget target) { + return entryMap.putIfAbsent(target, () => new CacheEntry()); + } + + TimestampedData getContents(Source source) => source.contents; + + noSuchMethod(Invocation invocation) { + print('noSuchMethod: ${invocation.memberName}'); + return super.noSuchMethod(invocation); + } +}