/* NSC -- new Scala compiler * Copyright 2005-2013 LAMP/EPFL * @author Martin Odersky */ package scala.tools.nsc package typechecker /** * @author Lukas Rytz * @version 1.0 */ trait AnalyzerPlugins { self: Analyzer => import global._ trait AnalyzerPlugin { /** * Selectively activate this analyzer plugin, e.g. according to the compiler phase. * * Note that the current phase can differ from the global compiler phase (look for `enteringPhase` * invocations in the compiler). For instance, lazy types created by the UnPickler are completed * at the phase in which their symbol is created. Observations show that this can even be the * parser phase. Since symbol completion can trigger subtyping, typing etc, your plugin might * need to be active also in phases other than namer and typer. * * Typically, this method can be implemented as * * global.phase.id < global.currentRun.picklerPhase.id */ def isActive(): Boolean = true /** * Let analyzer plugins change the expected type before type checking a tree. */ def pluginsPt(pt: Type, typer: Typer, tree: Tree, mode: Mode): Type = pt /** * Let analyzer plugins modify the type that has been computed for a tree. * * @param tpe The type inferred by the type checker, initially (for first plugin) `tree.tpe` * @param typer The typer that type checked `tree` * @param tree The type-checked tree * @param mode Mode that was used for typing `tree` * @param pt Expected type that was used for typing `tree` */ def pluginsTyped(tpe: Type, typer: Typer, tree: Tree, mode: Mode, pt: Type): Type = tpe /** * Let analyzer plugins change the types assigned to definitions. For definitions that have * an annotated type, the assigned type is obtained by typing that type tree. Otherwise, the * type is inferred by typing the definition's righthand side. * * In order to know if the type was inferred, you can query the `wasEmpty` field in the `tpt` * TypeTree of the definition (for DefDef and ValDef). * * (*) If the type of a method or value is inferred, the type-checked tree is stored in the * `analyzer.transformed` hash map, indexed by the definition's rhs tree. * * NOTE: Invoking the type checker can lead to cyclic reference errors. For instance, if this * method is called from the type completer of a recursive method, type checking the method * rhs will invoke the same completer again. It might be possible to avoid this situation by * assigning `tpe` to `defTree.symbol` (untested) - the final type computed by this method * will then be assigned to the definition's symbol by monoTypeCompleter (in Namers). * * The hooks into `typeSig` allow analyzer plugins to add annotations to (or change the types * of) definition symbols. This cannot not be achieved by using `pluginsTyped`: this method * is only called during type checking, so changing the type of a symbol at this point is too * late: references to the symbol might already be typed and therefore obtain the original * type assigned during naming. * * @param defTree is the definition for which the type was computed. The different cases are * outlined below. Note that this type is untyped (for methods and values with inferred type, * the typed rhs trees are available in analyzer.transformed). * * Case defTree: Template * - tpe : A ClassInfoType for the template * - typer: The typer for template members, i.e. expressions and definitions of defTree.body * - pt : WildcardType * - the class symbol is accessible through typer.context.owner * * Case defTree: ClassDef * - tpe : A ClassInfoType, or a PolyType(params, ClassInfoType) for polymorphic classes. * The class type is the one computed by templateSig, i.e. through the above case * - typer: The typer for the class. Note that this typer has a different context than the * typer for the template. * - pt : WildcardType * * Case defTree: ModuleDef * - tpe : A ClassInfoType computed by templateSig * - typer: The typer for the module. context.owner of this typer is the module class symbol * - pt : WildcardType * * Case defTree: DefDef * - tpe : The type of the method (MethodType, PolyType or NullaryMethodType). (*) * - typer: The typer the rhs of this method * - pt : If tpt.isEmpty, either the result type from the overridden method, or WildcardType. * Otherwise the type obtained from typing tpt. * - Note that for constructors, pt is the class type which the constructor creates. To type * check the rhs of the constructor however, the expected type has to be WildcardType (see * Typers.typedDefDef) * * Case defTree: ValDef * - tpe : The type of this value. (*) * - typer: The typer for the rhs of this value * - pt : If tpt.isEmpty, WildcardType. Otherwise the type obtained from typing tpt. * - Note that pluginsTypeSig might be called multiple times for the same ValDef since it is * used to compute the types of the accessor methods (see `pluginsTypeSigAccessor`) * * Case defTree: TypeDef * - tpe : The type obtained from typing rhs (PolyType if the TypeDef defines a polymorphic type) * - typer: The typer for the rhs of this type * - pt : WildcardType */ def pluginsTypeSig(tpe: Type, typer: Typer, defTree: Tree, pt: Type): Type = tpe /** * Modify the types of field accessors. The namer phase creates method types for getters and * setters based on the type of the corresponding field. * * Note: in order to compute the method type of an accessor, the namer calls `typeSig` on the * `ValDef` tree of the corresponding field. This implies that the `pluginsTypeSig` method * is potentially called multiple times for the same ValDef tree. * * @param tpe The method type created by the namer for the accessor * @param typer The typer for the ValDef (not for the rhs) * @param tree The ValDef corresponding to the accessor * @param sym The accessor method symbol (getter, setter, beanGetter or beanSetter) */ def pluginsTypeSigAccessor(tpe: Type, typer: Typer, tree: ValDef, sym: Symbol): Type = tpe /** * Decide whether this analyzer plugin can adapt a tree that has an annotated type to the * given type tp, taking into account the given mode (see method adapt in trait Typers). */ def canAdaptAnnotations(tree: Tree, typer: Typer, mode: Mode, pt: Type): Boolean = false /** * Adapt a tree that has an annotated type to the given type tp, taking into account the given * mode (see method adapt in trait Typers). * * An implementation cannot rely on canAdaptAnnotations being called before. If the implementing * class cannot do the adapting, it should return the tree unchanged. */ def adaptAnnotations(tree: Tree, typer: Typer, mode: Mode, pt: Type): Tree = tree /** * Modify the type of a return expression. By default, return expressions have type * NothingTpe. * * @param tpe The type of the return expression * @param typer The typer that was used for typing the return tree * @param tree The typed return expression tree * @param pt The return type of the enclosing method */ def pluginsTypedReturn(tpe: Type, typer: Typer, tree: Return, pt: Type): Type = tpe } /** * @define nonCumulativeReturnValueDoc Returns `None` if the plugin doesn't want to customize the default behavior * or something else if the plugin knows better that the implementation provided in scala-compiler.jar. * If multiple plugins return a non-empty result, it's going to be a compilation error. */ trait MacroPlugin { /** * Selectively activate this analyzer plugin, e.g. according to the compiler phase. * * Note that the current phase can differ from the global compiler phase (look for `enteringPhase` * invocations in the compiler). For instance, lazy types created by the UnPickler are completed * at the phase in which their symbol is created. Observations show that this can even be the * parser phase. Since symbol completion can trigger subtyping, typing etc, your plugin might * need to be active also in phases other than namer and typer. * * Typically, this method can be implemented as * * global.phase.id < global.currentRun.picklerPhase.id */ def isActive(): Boolean = true /** * Typechecks the right-hand side of a macro definition (which typically features * a mere reference to a macro implementation). * * Default implementation provided in `self.standardTypedMacroBody` makes sure that the rhs * resolves to a reference to a method in either a static object or a macro bundle, * verifies that the referred method is compatible with the macro def and upon success * attaches a macro impl binding to the macro def's symbol. * * $nonCumulativeReturnValueDoc. */ def pluginsTypedMacroBody(typer: Typer, ddef: DefDef): Option[Tree] = None /** * Figures out whether the given macro definition is blackbox or whitebox. * * Default implementation provided in `self.standardIsBlackbox` loads the macro impl binding * and fetches boxity from the "isBlackbox" field of the macro signature. * * $nonCumulativeReturnValueDoc. */ def pluginsIsBlackbox(macroDef: Symbol): Option[Boolean] = None /** * Expands an application of a def macro (i.e. of a symbol that has the MACRO flag set), * possibly using the current typer mode and the provided prototype. * * Default implementation provided in `self.standardMacroExpand` figures out whether the `expandee` * needs to be expanded right away or its expansion has to be delayed until all undetermined * parameters are inferred, then loads the macro implementation using `self.pluginsMacroRuntime`, * prepares the invocation arguments for the macro implementation using `self.pluginsMacroArgs`, * and finally calls into the macro implementation. After the call returns, it typechecks * the expansion and performs some bookkeeping. * * This method is typically implemented if your plugin requires significant changes to the macro engine. * If you only need to customize the macro context, consider implementing `pluginsMacroArgs`. * If you only need to customize how macro implementation are invoked, consider going for `pluginsMacroRuntime`. * * $nonCumulativeReturnValueDoc. */ def pluginsMacroExpand(typer: Typer, expandee: Tree, mode: Mode, pt: Type): Option[Tree] = None /** * Computes the arguments that need to be passed to the macro impl corresponding to a particular expandee. * * Default implementation provided in `self.standardMacroArgs` instantiates a `scala.reflect.macros.contexts.Context`, * gathers type and value arguments of the macro application and throws them together into `MacroArgs`. * * $nonCumulativeReturnValueDoc. */ def pluginsMacroArgs(typer: Typer, expandee: Tree): Option[MacroArgs] = None /** * Summons a function that encapsulates macro implementation invocations for a particular expandee. * * Default implementation provided in `self.standardMacroRuntime` returns a function that * loads the macro implementation binding from the macro definition symbol, * then uses either Java or Scala reflection to acquire the method that corresponds to the impl, * and then reflectively calls into that method. * * $nonCumulativeReturnValueDoc. */ def pluginsMacroRuntime(expandee: Tree): Option[MacroRuntime] = None /** * Creates a symbol for the given tree in lexical context encapsulated by the given namer. * * Default implementation provided in `namer.standardEnterSym` handles MemberDef's and Imports, * doing nothing for other trees (DocDef's are seen through and rewrapped). Typical implementation * of `enterSym` for a particular tree flavor creates a corresponding symbol, assigns it to the tree, * enters the symbol into scope and then might even perform some code generation. * * $nonCumulativeReturnValueDoc. */ def pluginsEnterSym(namer: Namer, tree: Tree): Boolean = false /** * Makes sure that for the given class definition, there exists a companion object definition. * * Default implementation provided in `namer.standardEnsureCompanionObject` looks up a companion symbol for the class definition * and then checks whether the resulting symbol exists or not. If it exists, then nothing else is done. * If not, a synthetic object definition is created using the provided factory, which is then entered into namer's scope. * * $nonCumulativeReturnValueDoc. */ def pluginsEnsureCompanionObject(namer: Namer, cdef: ClassDef, creator: ClassDef => Tree = companionModuleDef(_)): Option[Symbol] = None /** * Prepares a list of statements for being typechecked by performing domain-specific type-agnostic code synthesis. * * Trees passed into this method are going to be named, but not typed. * In particular, you can rely on the compiler having called `enterSym` on every stat prior to passing calling this method. * * Default implementation does nothing. Current approaches to code syntheses (generation of underlying fields * for getters/setters, creation of companion objects for case classes, etc) are too disparate and ad-hoc * to be treated uniformly, so I'm leaving this for future work. */ def pluginsEnterStats(typer: Typer, stats: List[Tree]): List[Tree] = stats } /** A list of registered analyzer plugins */ private var analyzerPlugins: List[AnalyzerPlugin] = Nil /** Registers a new analyzer plugin */ def addAnalyzerPlugin(plugin: AnalyzerPlugin) { if (!analyzerPlugins.contains(plugin)) analyzerPlugins = plugin :: analyzerPlugins } private abstract class CumulativeOp[T] { def default: T def accumulate: (T, AnalyzerPlugin) => T } private def invoke[T](op: CumulativeOp[T]): T = { if (analyzerPlugins.isEmpty) op.default else analyzerPlugins.foldLeft(op.default)((current, plugin) => if (!plugin.isActive()) current else op.accumulate(current, plugin)) } /** @see AnalyzerPlugin.pluginsPt */ def pluginsPt(pt: Type, typer: Typer, tree: Tree, mode: Mode): Type = // performance opt if (analyzerPlugins.isEmpty) pt else invoke(new CumulativeOp[Type] { def default = pt def accumulate = (pt, p) => p.pluginsPt(pt, typer, tree, mode) }) /** @see AnalyzerPlugin.pluginsTyped */ def pluginsTyped(tpe: Type, typer: Typer, tree: Tree, mode: Mode, pt: Type): Type = // performance opt if (analyzerPlugins.isEmpty) addAnnotations(tree, tpe) else invoke(new CumulativeOp[Type] { // support deprecated methods in annotation checkers def default = addAnnotations(tree, tpe) def accumulate = (tpe, p) => p.pluginsTyped(tpe, typer, tree, mode, pt) }) /** @see AnalyzerPlugin.pluginsTypeSig */ def pluginsTypeSig(tpe: Type, typer: Typer, defTree: Tree, pt: Type): Type = invoke(new CumulativeOp[Type] { def default = tpe def accumulate = (tpe, p) => p.pluginsTypeSig(tpe, typer, defTree, pt) }) /** @see AnalyzerPlugin.pluginsTypeSigAccessor */ def pluginsTypeSigAccessor(tpe: Type, typer: Typer, tree: ValDef, sym: Symbol): Type = invoke(new CumulativeOp[Type] { def default = tpe def accumulate = (tpe, p) => p.pluginsTypeSigAccessor(tpe, typer, tree, sym) }) /** @see AnalyzerPlugin.canAdaptAnnotations */ def canAdaptAnnotations(tree: Tree, typer: Typer, mode: Mode, pt: Type): Boolean = invoke(new CumulativeOp[Boolean] { // support deprecated methods in annotation checkers def default = global.canAdaptAnnotations(tree, mode, pt) def accumulate = (curr, p) => curr || p.canAdaptAnnotations(tree, typer, mode, pt) }) /** @see AnalyzerPlugin.adaptAnnotations */ def adaptAnnotations(tree: Tree, typer: Typer, mode: Mode, pt: Type): Tree = invoke(new CumulativeOp[Tree] { // support deprecated methods in annotation checkers def default = global.adaptAnnotations(tree, mode, pt) def accumulate = (tree, p) => p.adaptAnnotations(tree, typer, mode, pt) }) /** @see AnalyzerPlugin.pluginsTypedReturn */ def pluginsTypedReturn(tpe: Type, typer: Typer, tree: Return, pt: Type): Type = invoke(new CumulativeOp[Type] { def default = adaptTypeOfReturn(tree.expr, pt, tpe) def accumulate = (tpe, p) => p.pluginsTypedReturn(tpe, typer, tree, pt) }) /** A list of registered macro plugins */ private var macroPlugins: List[MacroPlugin] = Nil /** Registers a new macro plugin */ def addMacroPlugin(plugin: MacroPlugin) { if (!macroPlugins.contains(plugin)) macroPlugins = plugin :: macroPlugins } private abstract class NonCumulativeOp[T] { def position: Position def description: String def default: T def custom(plugin: MacroPlugin): Option[T] } private def invoke[T](op: NonCumulativeOp[T]): T = { if (macroPlugins.isEmpty) op.default else { val results = macroPlugins.filter(_.isActive()).map(plugin => (plugin, op.custom(plugin))) results.flatMap { case (p, Some(result)) => Some((p, result)); case _ => None } match { case (p1, _) :: (p2, _) :: _ => typer.context.error(op.position, s"both $p1 and $p2 want to ${op.description}"); op.default case (_, custom) :: Nil => custom case Nil => op.default } } } /** @see MacroPlugin.pluginsTypedMacroBody */ def pluginsTypedMacroBody(typer: Typer, ddef: DefDef): Tree = invoke(new NonCumulativeOp[Tree] { def position = ddef.pos def description = "typecheck this macro definition" def default = standardTypedMacroBody(typer, ddef) def custom(plugin: MacroPlugin) = plugin.pluginsTypedMacroBody(typer, ddef) }) /** @see MacroPlugin.pluginsIsBlackbox */ def pluginsIsBlackbox(macroDef: Symbol): Boolean = invoke(new NonCumulativeOp[Boolean] { def position = macroDef.pos def description = "compute boxity for this macro definition" def default = standardIsBlackbox(macroDef) def custom(plugin: MacroPlugin) = plugin.pluginsIsBlackbox(macroDef) }) /** @see MacroPlugin.pluginsMacroExpand */ def pluginsMacroExpand(typer: Typer, expandee: Tree, mode: Mode, pt: Type): Tree = invoke(new NonCumulativeOp[Tree] { def position = expandee.pos def description = "expand this macro application" def default = standardMacroExpand(typer, expandee, mode, pt) def custom(plugin: MacroPlugin) = plugin.pluginsMacroExpand(typer, expandee, mode, pt) }) /** @see MacroPlugin.pluginsMacroArgs */ def pluginsMacroArgs(typer: Typer, expandee: Tree): MacroArgs = invoke(new NonCumulativeOp[MacroArgs] { def position = expandee.pos def description = "compute macro arguments for this macro application" def default = standardMacroArgs(typer, expandee) def custom(plugin: MacroPlugin) = plugin.pluginsMacroArgs(typer, expandee) }) /** @see MacroPlugin.pluginsMacroRuntime */ def pluginsMacroRuntime(expandee: Tree): MacroRuntime = invoke(new NonCumulativeOp[MacroRuntime] { def position = expandee.pos def description = "compute macro runtime for this macro application" def default = standardMacroRuntime(expandee) def custom(plugin: MacroPlugin) = plugin.pluginsMacroRuntime(expandee) }) /** @see MacroPlugin.pluginsEnterSym */ def pluginsEnterSym(namer: Namer, tree: Tree): Context = if (macroPlugins.isEmpty) namer.standardEnterSym(tree) else invoke(new NonCumulativeOp[Context] { def position = tree.pos def description = "enter a symbol for this tree" def default = namer.standardEnterSym(tree) def custom(plugin: MacroPlugin) = { val hasExistingSym = tree.symbol != NoSymbol val result = plugin.pluginsEnterSym(namer, tree) if (result && hasExistingSym) Some(namer.context) else if (result && tree.isInstanceOf[Import]) Some(namer.context.make(tree)) else if (result) Some(namer.context) else None } }) /** @see MacroPlugin.pluginsEnsureCompanionObject */ def pluginsEnsureCompanionObject(namer: Namer, cdef: ClassDef, creator: ClassDef => Tree = companionModuleDef(_)): Symbol = invoke(new NonCumulativeOp[Symbol] { def position = cdef.pos def description = "enter a companion symbol for this tree" def default = namer.standardEnsureCompanionObject(cdef, creator) def custom(plugin: MacroPlugin) = plugin.pluginsEnsureCompanionObject(namer, cdef, creator) }) /** @see MacroPlugin.pluginsEnterStats */ def pluginsEnterStats(typer: Typer, stats: List[Tree]): List[Tree] = { // performance opt if (macroPlugins.isEmpty) stats else macroPlugins.foldLeft(stats)((current, plugin) => if (!plugin.isActive()) current else plugin.pluginsEnterStats(typer, current)) } }