OLD | NEW |
(Empty) | |
| 1 library di.transformer.injector_generator; |
| 2 |
| 3 import 'dart:async'; |
| 4 import 'package:analyzer/src/generated/ast.dart'; |
| 5 import 'package:analyzer/src/generated/element.dart'; |
| 6 import 'package:barback/barback.dart'; |
| 7 import 'package:code_transformers/resolver.dart'; |
| 8 import 'package:di/transformer/options.dart'; |
| 9 import 'package:path/path.dart' as path; |
| 10 |
| 11 import 'refactor.dart'; |
| 12 |
| 13 /** |
| 14 * Pub transformer which generates type factories for all injectable types |
| 15 * in the application. |
| 16 */ |
| 17 class InjectorGenerator extends Transformer with ResolverTransformer { |
| 18 final TransformOptions options; |
| 19 |
| 20 InjectorGenerator(this.options, Resolvers resolvers) { |
| 21 this.resolvers = resolvers; |
| 22 } |
| 23 |
| 24 Future<bool> shouldApplyResolver(Asset asset) => options.isDartEntry(asset); |
| 25 |
| 26 applyResolver(Transform transform, Resolver resolver) => |
| 27 new _Processor(transform, resolver, options).process(); |
| 28 } |
| 29 |
| 30 /** Class for processing a single apply.*/ |
| 31 class _Processor { |
| 32 |
| 33 /** Current transform. */ |
| 34 final Transform transform; |
| 35 |
| 36 final Resolver resolver; |
| 37 final TransformOptions options; |
| 38 |
| 39 /** Asset ID for the location of the generated file, for imports. */ |
| 40 AssetId _generatedAssetId; |
| 41 |
| 42 /** Resolved injectable annotations of the form `@Injectable()`. */ |
| 43 final List<TopLevelVariableElement> injectableMetaConsts = |
| 44 <TopLevelVariableElement>[]; |
| 45 |
| 46 /** Resolved injectable annotations of the form `@injectable`. */ |
| 47 final List<ConstructorElement> injectableMetaConstructors = |
| 48 <ConstructorElement>[]; |
| 49 |
| 50 /** Default list of injectable consts */ |
| 51 static const List<String> defaultInjectableMetaConsts = const [ |
| 52 'inject.inject' |
| 53 ]; |
| 54 |
| 55 _Processor(this.transform, this.resolver, this.options); |
| 56 |
| 57 TransformLogger get logger => transform.logger; |
| 58 |
| 59 process() { |
| 60 _resolveInjectableMetadata(); |
| 61 |
| 62 var id = transform.primaryInput.id; |
| 63 var outputFilename = '${path.url.basenameWithoutExtension(id.path)}' |
| 64 '_static_injector.dart'; |
| 65 var outputPath = path.url.join(path.url.dirname(id.path), outputFilename); |
| 66 _generatedAssetId = new AssetId(id.package, outputPath); |
| 67 |
| 68 var constructors = _gatherConstructors(); |
| 69 |
| 70 var injectLibContents = _generateInjectLibrary(constructors); |
| 71 transform.addOutput( |
| 72 new Asset.fromString(_generatedAssetId, injectLibContents)); |
| 73 |
| 74 transformIdentifiers(transform, resolver, |
| 75 identifier: 'di.auto_injector.defaultInjector', |
| 76 replacement: 'createStaticInjector', |
| 77 importPrefix: 'generated_static_injector', |
| 78 importUrl: outputFilename); |
| 79 } |
| 80 |
| 81 /** Resolves the classes for the injectable annotations in the current AST. */ |
| 82 void _resolveInjectableMetadata() { |
| 83 for (var constName in defaultInjectableMetaConsts) { |
| 84 var variable = resolver.getLibraryVariable(constName); |
| 85 if (variable != null) { |
| 86 injectableMetaConsts.add(variable); |
| 87 } |
| 88 } |
| 89 |
| 90 // Resolve the user-specified annotations |
| 91 // These may be either type names (constructors) or consts. |
| 92 for (var metaName in options.injectableAnnotations) { |
| 93 var variable = resolver.getLibraryVariable(metaName); |
| 94 if (variable != null) { |
| 95 injectableMetaConsts.add(variable); |
| 96 continue; |
| 97 } |
| 98 var cls = resolver.getType(metaName); |
| 99 if (cls != null && cls.unnamedConstructor != null) { |
| 100 injectableMetaConstructors.add(cls.unnamedConstructor); |
| 101 continue; |
| 102 } |
| 103 if (!DEFAULT_INJECTABLE_ANNOTATIONS.contains(metaName)) { |
| 104 logger.warning('Unable to resolve injectable annotation $metaName'); |
| 105 } |
| 106 } |
| 107 } |
| 108 |
| 109 /** Finds all annotated constructors or annotated classes in the program. */ |
| 110 Iterable<ConstructorElement> _gatherConstructors() { |
| 111 var constructors = resolver.libraries |
| 112 .expand((lib) => lib.units) |
| 113 .expand((compilationUnit) => compilationUnit.types) |
| 114 .map(_findInjectedConstructor) |
| 115 .where((ctor) => ctor != null).toList(); |
| 116 |
| 117 constructors.addAll(_gatherInjectablesContents()); |
| 118 constructors.addAll(_gatherManuallyInjected()); |
| 119 |
| 120 return constructors.toSet(); |
| 121 } |
| 122 |
| 123 /** |
| 124 * Get the constructors for all elements in the library @Injectables |
| 125 * statements. These are used to mark types as injectable which would |
| 126 * otherwise not be injected. |
| 127 * |
| 128 * Syntax is: |
| 129 * |
| 130 * @Injectables(const[ElementName]) |
| 131 * library my.library; |
| 132 */ |
| 133 Iterable<ConstructorElement> _gatherInjectablesContents() { |
| 134 var injectablesClass = resolver.getType('di.annotations.Injectables'); |
| 135 if (injectablesClass == null) return const []; |
| 136 var injectablesCtor = injectablesClass.unnamedConstructor; |
| 137 |
| 138 var ctors = []; |
| 139 |
| 140 for (var lib in resolver.libraries) { |
| 141 var annotationIdx = 0; |
| 142 for (var annotation in lib.metadata) { |
| 143 if (annotation.element == injectablesCtor) { |
| 144 var libDirective = lib.definingCompilationUnit.node.directives |
| 145 .where((d) => d is LibraryDirective).single; |
| 146 var annotationDirective = libDirective.metadata[annotationIdx]; |
| 147 var listLiteral = annotationDirective.arguments.arguments.first; |
| 148 |
| 149 for (var expr in listLiteral.elements) { |
| 150 var element = (expr as SimpleIdentifier).bestElement; |
| 151 if (element == null || element is! ClassElement) { |
| 152 _warn('Unable to resolve class $expr', element); |
| 153 continue; |
| 154 } |
| 155 var ctor = _findInjectedConstructor(element, true); |
| 156 if (ctor != null) { |
| 157 ctors.add(ctor); |
| 158 } |
| 159 } |
| 160 } |
| 161 } |
| 162 } |
| 163 return ctors; |
| 164 } |
| 165 |
| 166 /** |
| 167 * Finds all types which were manually specified as being injected in |
| 168 * the options file. |
| 169 */ |
| 170 Iterable<ConstructorElement> _gatherManuallyInjected() { |
| 171 var ctors = []; |
| 172 for (var injectedName in options.injectedTypes) { |
| 173 var injectedClass = resolver.getType(injectedName); |
| 174 if (injectedClass == null) { |
| 175 logger.warning('Unable to resolve injected type name $injectedName'); |
| 176 continue; |
| 177 } |
| 178 var ctor = _findInjectedConstructor(injectedClass, true); |
| 179 if (ctor != null) { |
| 180 ctors.add(ctor); |
| 181 } |
| 182 } |
| 183 return ctors; |
| 184 } |
| 185 |
| 186 /** |
| 187 * Checks if the element is annotated with one of the known injectablee |
| 188 * annotations. |
| 189 */ |
| 190 bool _isElementAnnotated(Element e) { |
| 191 for (var meta in e.metadata) { |
| 192 if (meta.element is PropertyAccessorElement && |
| 193 injectableMetaConsts.contains(meta.element.variable)) { |
| 194 return true; |
| 195 } else if (meta.element is ConstructorElement && |
| 196 injectableMetaConstructors.contains(meta.element)) { |
| 197 return true; |
| 198 } |
| 199 } |
| 200 return false; |
| 201 } |
| 202 |
| 203 /** |
| 204 * Find an 'injected' constructor for the given class. |
| 205 * If [noAnnotation] is true then this will assume that the class is marked |
| 206 * for injection and will use the default constructor. |
| 207 */ |
| 208 ConstructorElement _findInjectedConstructor(ClassElement cls, |
| 209 [bool noAnnotation = false]) { |
| 210 var classInjectedConstructors = []; |
| 211 if (_isElementAnnotated(cls) || noAnnotation) { |
| 212 var defaultConstructor = cls.unnamedConstructor; |
| 213 if (defaultConstructor == null) { |
| 214 _warn('${cls.name} cannot be injected because ' |
| 215 'it does not have a default constructor.', cls); |
| 216 } else { |
| 217 classInjectedConstructors.add(defaultConstructor); |
| 218 } |
| 219 } |
| 220 |
| 221 classInjectedConstructors.addAll( |
| 222 cls.constructors.where(_isElementAnnotated)); |
| 223 |
| 224 if (classInjectedConstructors.isEmpty) return null; |
| 225 if (classInjectedConstructors.length > 1) { |
| 226 _warn('${cls.name} has more than one constructor annotated for ' |
| 227 'injection.', cls); |
| 228 return null; |
| 229 } |
| 230 |
| 231 var ctor = classInjectedConstructors.single; |
| 232 if (!_validateConstructor(ctor)) return null; |
| 233 |
| 234 return ctor; |
| 235 } |
| 236 |
| 237 /** |
| 238 * Validates that the constructor is injectable and emits warnings for any |
| 239 * errors. |
| 240 */ |
| 241 bool _validateConstructor(ConstructorElement ctor) { |
| 242 var cls = ctor.enclosingElement; |
| 243 if (cls.isAbstract && !ctor.isFactory) { |
| 244 _warn('${cls.name} cannot be injected because ' |
| 245 'it is an abstract type with no factory constructor.', cls); |
| 246 return false; |
| 247 } |
| 248 if (cls.isPrivate) { |
| 249 _warn('${cls.name} cannot be injected because it is a private type.', |
| 250 cls); |
| 251 return false; |
| 252 } |
| 253 if (resolver.getImportUri(cls.library, from: _generatedAssetId) == null) { |
| 254 _warn('${cls.name} cannot be injected because ' |
| 255 'the containing file cannot be imported.', cls); |
| 256 return false; |
| 257 } |
| 258 if (!cls.typeParameters.isEmpty) { |
| 259 _warn('${cls.name} is a parameterized type.', cls); |
| 260 // Only warn. |
| 261 } |
| 262 if (ctor.name != '') { |
| 263 _warn('Named constructors cannot be injected.', ctor); |
| 264 return false; |
| 265 } |
| 266 for (var param in ctor.parameters) { |
| 267 var type = param.type; |
| 268 if (type is InterfaceType && |
| 269 type.typeArguments.any((t) => !t.isDynamic)) { |
| 270 _warn('${cls.name} cannot be injected because ' |
| 271 '${param.type} is a parameterized type.', ctor); |
| 272 return false; |
| 273 } |
| 274 if (type.isDynamic) { |
| 275 _warn('${cls.name} cannot be injected because parameter type ' |
| 276 '${param.name} cannot be resolved.', ctor); |
| 277 return false; |
| 278 } |
| 279 } |
| 280 return true; |
| 281 } |
| 282 |
| 283 /** |
| 284 * Creates a library file for the specified constructors. |
| 285 */ |
| 286 String _generateInjectLibrary(Iterable<ConstructorElement> constructors) { |
| 287 var prefixes = <LibraryElement, String>{}; |
| 288 |
| 289 var ctorTypes = constructors.map((ctor) => ctor.enclosingElement).toSet(); |
| 290 var paramTypes = constructors.expand((ctor) => ctor.parameters) |
| 291 .map((param) => param.type.element).toSet(); |
| 292 |
| 293 var usedLibs = new Set<LibraryElement>(); |
| 294 String resolveClassName(ClassElement type) { |
| 295 var library = type.library; |
| 296 usedLibs.add(library); |
| 297 |
| 298 var prefix = prefixes[library]; |
| 299 if (prefix == null) { |
| 300 prefix = prefixes[library] = |
| 301 library.isDartCore ? '' : 'import_${prefixes.length}'; |
| 302 } |
| 303 if (prefix.isNotEmpty) { |
| 304 prefix = '$prefix.'; |
| 305 } |
| 306 return '$prefix${type.name}'; |
| 307 } |
| 308 |
| 309 var factoriesBuffer = new StringBuffer(); |
| 310 for (var ctor in constructors) { |
| 311 var type = ctor.enclosingElement; |
| 312 var typeName = resolveClassName(type); |
| 313 factoriesBuffer.write(' $typeName: (f) => new $typeName('); |
| 314 var params = ctor.parameters.map((param) { |
| 315 var typeName = resolveClassName(param.type.element); |
| 316 var annotations = []; |
| 317 if (param.metadata.isNotEmpty) { |
| 318 annotations = param.metadata.map( |
| 319 (item) => resolveClassName(item.element.returnType.element)); |
| 320 } |
| 321 var annotationsSuffix = |
| 322 annotations.isNotEmpty ? ', ${annotations.first}' : ''; |
| 323 return 'f($typeName$annotationsSuffix)'; |
| 324 }); |
| 325 factoriesBuffer.write('${params.join(', ')}),\n'); |
| 326 } |
| 327 |
| 328 var outputBuffer = new StringBuffer(); |
| 329 |
| 330 _writeStaticInjectorHeader(transform.primaryInput.id, outputBuffer); |
| 331 usedLibs.forEach((lib) { |
| 332 if (lib.isDartCore) return; |
| 333 var uri = resolver.getImportUri(lib, from: _generatedAssetId); |
| 334 outputBuffer.write('import \'$uri\' as ${prefixes[lib]};\n'); |
| 335 }); |
| 336 _writePreamble(outputBuffer); |
| 337 outputBuffer.write(factoriesBuffer); |
| 338 _writeFooter(outputBuffer); |
| 339 |
| 340 return outputBuffer.toString(); |
| 341 } |
| 342 |
| 343 void _warn(String msg, Element element) { |
| 344 logger.warning(msg, asset: resolver.getSourceAssetId(element), |
| 345 span: resolver.getSourceSpan(element)); |
| 346 } |
| 347 } |
| 348 |
| 349 void _writeStaticInjectorHeader(AssetId id, StringSink sink) { |
| 350 var libName = path.withoutExtension(id.path).replaceAll('/', '.'); |
| 351 libName = libName.replaceAll('-', '_'); |
| 352 sink.write(''' |
| 353 library ${id.package}.$libName.generated_static_injector; |
| 354 |
| 355 import 'package:di/di.dart'; |
| 356 import 'package:di/static_injector.dart'; |
| 357 |
| 358 '''); |
| 359 } |
| 360 |
| 361 void _writePreamble(StringSink sink) { |
| 362 sink.write(''' |
| 363 Injector createStaticInjector({List<Module> modules, String name, |
| 364 bool allowImplicitInjection: false}) => |
| 365 new StaticInjector(modules: modules, name: name, |
| 366 allowImplicitInjection: allowImplicitInjection, |
| 367 typeFactories: factories); |
| 368 |
| 369 final Map<Type, TypeFactory> factories = <Type, TypeFactory>{ |
| 370 '''); |
| 371 } |
| 372 |
| 373 void _writeFooter(StringSink sink) { |
| 374 sink.write(''' |
| 375 }; |
| 376 '''); |
| 377 } |
OLD | NEW |