├── .sbtopts ├── project ├── build.properties ├── plugins.sbt └── Formatting.scala ├── rules ├── akka │ └── src │ │ ├── main │ │ ├── resources │ │ │ └── abide-plugin.xml │ │ └── scala │ │ │ └── com │ │ │ └── typesafe │ │ │ └── abide │ │ │ └── akka │ │ │ └── SenderInFuture.scala │ │ └── test │ │ └── scala │ │ └── com │ │ └── typesafe │ │ └── abide │ │ └── akka │ │ └── SenderInFutureTest.scala ├── extra │ └── src │ │ ├── main │ │ ├── resources │ │ │ └── abide-plugin.xml │ │ └── scala │ │ │ └── com │ │ │ └── typesafe │ │ │ └── abide │ │ │ └── extra │ │ │ ├── InstanceOfUsed.scala │ │ │ └── FixedNameOverrides.scala │ │ └── test │ │ └── scala │ │ └── com │ │ └── typesafe │ │ └── abide │ │ └── extra │ │ ├── InstanceOfUsedTest.scala │ │ └── FixedNameOverridesTest.scala └── core │ └── src │ ├── main │ ├── resources │ │ └── abide-plugin.xml │ └── scala │ │ └── com │ │ └── lightbend │ │ └── abide │ │ └── core │ │ ├── NullaryOverride.scala │ │ ├── StupidRecursion.scala │ │ ├── ByNameRightAssociative.scala │ │ ├── DelayedInitSelect.scala │ │ ├── PolyImplicitOverload.scala │ │ ├── PackageObjectClasses.scala │ │ ├── ValueClassSynchronized.scala │ │ ├── RenamedDefaultParameter.scala │ │ ├── EmptyOrNonEmptyUsingSize.scala │ │ ├── NullaryUnit.scala │ │ ├── PrivateShadow.scala │ │ ├── MatchCaseOnSeq.scala │ │ ├── InferAny.scala │ │ ├── ValInsteadOfVar.scala │ │ ├── PublicMutable.scala │ │ ├── UnusedMember.scala │ │ ├── TypeParameterShadow.scala │ │ └── Inaccessible.scala │ └── test │ ├── scala │ └── com │ │ └── lightbend │ │ └── abide │ │ └── core │ │ ├── StupidRecursionTest.scala │ │ ├── PackageObjectClassesTest.scala │ │ ├── ValueClassSynchronizedTest.scala │ │ ├── MemberValInsteadOfVarTest.scala │ │ ├── NullaryUnitTest.scala │ │ ├── LocalValInsteadOfVarTest.scala │ │ ├── MatchCaseOnSeqTest.scala │ │ ├── InferAnyTest.scala │ │ ├── InaccessibleTest.scala │ │ ├── PolyImplicitOverloadTest.scala │ │ ├── RenamedDefaultParameterTest.scala │ │ ├── PrivateShadowTest.scala │ │ ├── ByNameRightAssociativeTest.scala │ │ ├── DelayedInitSelectTest.scala │ │ ├── PublicMutableTest.scala │ │ ├── NullaryOverrideTest.scala │ │ ├── EmptyOrNonEmptyUsingSizeTest.scala │ │ ├── TypeParameterShadowTest.scala │ │ └── UnusedMemberTest.scala │ └── resources │ └── MurmurHash.scala ├── abide └── src │ ├── main │ ├── resources │ │ ├── scalac-plugin.xml │ │ └── abide-plugin.xml │ └── scala │ │ └── scala │ │ └── tools │ │ └── abide │ │ ├── Warning.scala │ │ ├── presentation │ │ ├── ConsolePresenter.scala │ │ └── Presenter.scala │ │ ├── traversal │ │ ├── WarningRule.scala │ │ ├── ScopingRule.scala │ │ ├── ExistentialRule.scala │ │ ├── PathRule.scala │ │ ├── NaiveTraversalAnalyzer.scala │ │ ├── TraversalRule.scala │ │ └── FusingTraversalAnalyzer.scala │ │ ├── Context.scala │ │ ├── Abide.scala │ │ ├── Analyzer.scala │ │ ├── Rule.scala │ │ ├── directives │ │ └── MutabilityChecker.scala │ │ └── compiler │ │ └── AbidePlugin.scala │ └── test │ └── scala │ └── scala │ └── tools │ └── abide │ ├── AbideTest.scala │ ├── traversal │ ├── TraversalTest.scala │ ├── WarningRuleTest.scala │ └── ErroneousRuleTest.scala │ └── directives │ └── MutabilityTest.scala ├── README.md ├── .gitignore ├── macros └── src │ ├── main │ └── scala │ │ └── scala │ │ └── reflect │ │ └── internal │ │ └── traversal │ │ ├── Fuse.scala │ │ ├── ScopingTraversalFusion.scala │ │ ├── ScopingTraversal.scala │ │ ├── TraversalFusion.scala │ │ └── Traversal.scala │ └── test │ ├── scala │ └── scala │ │ └── reflect │ │ └── internal │ │ └── traversal │ │ ├── OrderingTest.scala │ │ ├── CompilerProvider.scala │ │ ├── TreeProvider.scala │ │ ├── TraversalTest.scala │ │ └── SpeedAnalysis.scala │ └── resources │ └── traversal │ ├── AddressBook.scala │ └── SimpleInterpreter.scala ├── wiki ├── extra-rules.md ├── akka-rules.md ├── traversal │ ├── scoping-rules.md │ ├── existential-rules.md │ ├── warning-rules.md │ └── traversal-rules.md ├── rules.md └── extensions.md ├── LICENSE ├── sbt-plugin └── src │ └── main │ └── scala │ └── scala │ └── tools │ └── abide │ └── AbideSbtPlugin.scala └── README-old.md /.sbtopts: -------------------------------------------------------------------------------- 1 | -J-Xmx2048m 2 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version = 0.13.13 2 | -------------------------------------------------------------------------------- /rules/akka/src/main/resources/abide-plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scalariform" % "sbt-scalariform" % "1.6.0") 2 | 3 | addSbtPlugin("com.eed3si9n" % "sbt-doge" % "0.1.5") 4 | -------------------------------------------------------------------------------- /abide/src/main/resources/scalac-plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | abide 3 | scala.tools.abide.compiler.AbidePlugin 4 | 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RIP Abide 2 | 3 | This project has been superseded by [Scalafix](https://github.com/scalacenter/scalafix), 4 | the refactoring and linting tool for Scala. 5 | -------------------------------------------------------------------------------- /rules/extra/src/main/resources/abide-plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | core/project/target 2 | core/target 3 | rules/project/target 4 | rules/target 5 | project/target 6 | target 7 | *.swp 8 | *.BAK 9 | .classpath 10 | .project 11 | *.prefs 12 | .cache 13 | .tmpBin/ -------------------------------------------------------------------------------- /abide/src/test/scala/scala/tools/abide/AbideTest.scala: -------------------------------------------------------------------------------- 1 | package scala.tools.abide 2 | 3 | import scala.reflect.internal.traversal._ 4 | import org.scalatest._ 5 | 6 | abstract class AbideTest extends FlatSpec with Matchers with TreeProvider 7 | -------------------------------------------------------------------------------- /abide/src/main/resources/abide-plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /abide/src/main/scala/scala/tools/abide/Warning.scala: -------------------------------------------------------------------------------- 1 | package scala.tools.abide 2 | 3 | /** 4 | * Base trait for warnings discovered by the verification framework. 5 | * 6 | * Each warning has a position and a rule from which it comes. This rule 7 | * can then be used to add extra capabilities to the warning (like 8 | * quickfix). 9 | */ 10 | trait Warning { 11 | val rule: Rule 12 | val pos: rule.context.universe.Position 13 | val message: String 14 | } 15 | -------------------------------------------------------------------------------- /rules/core/src/main/resources/abide-plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /rules/extra/src/test/scala/com/typesafe/abide/extra/InstanceOfUsedTest.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.extra 2 | 3 | import scala.tools.abide.traversal.TraversalTest 4 | 5 | class InstanceOfUsedTest extends TraversalTest { 6 | 7 | val rule = new InstanceOfUsed(context) 8 | 9 | "usages of asInstanceOf[T]" should "give a warning" in { 10 | val tree = fromString(""" 11 | class Test { 12 | val a = 1.asInstanceOf[Any] 13 | }""") 14 | 15 | global.ask { () => apply(rule)(tree).size should be(1) } 16 | } 17 | 18 | "usages of isInstanceOf[T]" should "give a warning" in { 19 | val tree = fromString(""" 20 | class Test { 21 | val a = 1.isInstanceOf[Any] 22 | }""") 23 | 24 | global.ask { () => apply(rule)(tree).size should be(1) } 25 | } 26 | 27 | } 28 | -------------------------------------------------------------------------------- /rules/core/src/main/scala/com/lightbend/abide/core/NullaryOverride.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide._ 4 | import scala.tools.abide.traversal._ 5 | 6 | class NullaryOverride(val context: Context) extends WarningRule { 7 | import context.universe._ 8 | 9 | val name = "nullary-override" 10 | 11 | case class Warning(defDef: DefDef) extends RuleWarning { 12 | val pos = defDef.pos 13 | val message = "Non-nullary method overrides nullary method" 14 | } 15 | 16 | val step = optimize { 17 | case defDef @ DefDef(mods, name, tparams, List(List()), tpt, rhs) if defDef.symbol.asMethod.isOverride => 18 | val overrides = defDef.symbol.overrides 19 | if (overrides.exists(_.paramLists.isEmpty)) { 20 | nok(Warning(defDef)) 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /rules/extra/src/main/scala/com/typesafe/abide/extra/InstanceOfUsed.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.extra 2 | 3 | import scala.tools.abide._ 4 | import scala.tools.abide.traversal._ 5 | 6 | class InstanceOfUsed(val context: Context) extends WarningRule { 7 | import context.universe._ 8 | 9 | val name = "instance-of-used" 10 | 11 | case class Warning(tree: Tree, which: String) extends RuleWarning { 12 | val pos = tree.pos 13 | val message = s"Using $which. Consider using pattern matching instead." 14 | } 15 | 16 | private val AsInstanceOf = TermName("asInstanceOf") 17 | private val IsInstanceOf = TermName("isInstanceOf") 18 | val step = optimize { 19 | case appl @ Select(_, AsInstanceOf) => 20 | nok(Warning(appl, "asInstanceOf")) 21 | 22 | case appl @ Select(_, IsInstanceOf) => 23 | nok(Warning(appl, "isInstanceOf")) 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /abide/src/test/scala/scala/tools/abide/traversal/TraversalTest.scala: -------------------------------------------------------------------------------- 1 | package scala.tools.abide.traversal 2 | 3 | import scala.tools.abide._ 4 | import scala.tools.abide.traversal._ 5 | import scala.tools.abide.directives._ 6 | import scala.reflect.internal.traversal._ 7 | 8 | trait TraversalTest extends AbideTest { 9 | 10 | val context = new Context(global) with MutabilityChecker 11 | 12 | def apply(rule: TraversalRule)(tree: global.Tree): List[rule.Warning] = { 13 | rule.traverse(tree.asInstanceOf[rule.universe.Tree]) 14 | rule.result.warnings 15 | } 16 | 17 | def apply(rules: TraversalRule*)(tree: global.Tree): List[Warning] = { 18 | val ruleCast = rules.map(_.asInstanceOf[Traversal { val universe: TraversalTest.this.global.type }]) 19 | val fusion = Fuse(global)(ruleCast: _*) 20 | fusion.traverse(tree) 21 | rules.flatMap(_.result.warnings).toList 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /rules/core/src/main/scala/com/lightbend/abide/core/StupidRecursion.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide._ 4 | import scala.tools.abide.traversal._ 5 | 6 | class StupidRecursion(val context: Context) extends ScopingRule { 7 | import context.universe._ 8 | 9 | type Owner = Symbol 10 | 11 | val name = "stupid-recursion" 12 | 13 | case class Warning(tree: Tree) extends RuleWarning { 14 | val pos = tree.pos 15 | val message = s"The value $tree is recursively used in its directly defining scope" 16 | } 17 | 18 | val step = optimize { 19 | case defDef @ q"def $name : $tpt = $body" => enter(defDef.symbol) 20 | case id @ Ident(_) if id.symbol != null && (state childOf id.symbol) => nok(Warning(id)) 21 | case s @ Select(_, _) if s.symbol != null && (state childOf s.symbol) => nok(Warning(s)) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /abide/src/main/scala/scala/tools/abide/presentation/ConsolePresenter.scala: -------------------------------------------------------------------------------- 1 | package scala.tools.abide.presentation 2 | 3 | import scala.tools.nsc._ 4 | import scala.tools.abide._ 5 | 6 | /** 7 | * ConsolePresenterGenerator 8 | * 9 | * @see [[ConsolePresenter]] 10 | */ 11 | object ConsolePresenterGenerator extends PresenterGenerator { 12 | def getPresenter(global: Global): ConsolePresenter = { 13 | new ConsolePresenter(global) 14 | } 15 | } 16 | 17 | /** 18 | * ConsolePresenter 19 | * 20 | * Simple [[Presenter]] that outputs warnings as compiler warnings 21 | */ 22 | class ConsolePresenter(protected val global: Global) extends Presenter { 23 | import global._ 24 | 25 | /** Outputs Abide warnings as compiler warnings */ 26 | def apply(unit: CompilationUnit, warnings: List[Warning]): Unit = { 27 | warnings.foreach { warning => 28 | global.warning(warning.pos, warning.message) 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /rules/core/src/main/scala/com/lightbend/abide/core/ByNameRightAssociative.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide._ 4 | import scala.tools.abide.traversal._ 5 | 6 | class ByNameRightAssociative(val context: Context) extends WarningRule { 7 | import context.universe._ 8 | 9 | val name = "by-name-right-associative" 10 | 11 | case class Warning(defDef: DefDef) extends RuleWarning { 12 | val pos = defDef.pos 13 | val message = "By-name parameters will be evaluated eagerly when used in right-associative infix operators. For more details, see SI-1980." 14 | } 15 | 16 | def isByName(param: Symbol) = param.tpe.typeSymbol == definitions.ByNameParamClass 17 | 18 | val step = optimize { 19 | case defDef @ DefDef(_, name, _, params :: _, _, _) => 20 | if (!treeInfo.isLeftAssoc(name.decodedName) && params.exists(p => isByName(p.symbol))) { 21 | nok(Warning(defDef)) 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /rules/core/src/main/scala/com/lightbend/abide/core/DelayedInitSelect.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide._ 4 | import scala.tools.abide.traversal._ 5 | 6 | class DelayedInitSelect(val context: Context) extends WarningRule { 7 | import context.universe._ 8 | 9 | val name = "delayed-init-select" 10 | 11 | case class Warning(symbol: Symbol) extends RuleWarning { 12 | val pos = symbol.pos 13 | val message = 14 | s"Selecting $symbol from ${symbol.owner} which extends scala.DelayedInit is likely to yield an uninitialized value" 15 | } 16 | 17 | val step = optimize { 18 | case sel @ Select(qual, _) => 19 | val symbol = sel.symbol 20 | val isLikelyUninitialized = 21 | (symbol.owner.tpe <:< typeOf[scala.DelayedInit]) && 22 | !qual.tpe.isInstanceOf[ThisType] && 23 | symbol.accessedOrSelf.isVal 24 | 25 | if (isLikelyUninitialized) { 26 | nok(Warning(symbol)) 27 | } 28 | 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /macros/src/main/scala/scala/reflect/internal/traversal/Fuse.scala: -------------------------------------------------------------------------------- 1 | package scala.reflect.internal.traversal 2 | 3 | /** 4 | * object Fuse 5 | * 6 | * Fuses multiple traversals in a way that increases overall traversal speed. 7 | * We use the type information we extracted in [[Traversal]] with [[OptimizingMacros]] 8 | * to quickly determine which rules should be applied to the tree we're currently 9 | * visiting. 10 | * 11 | * @see [[Traversal]] 12 | * @see [[OptimizingMacros]] 13 | */ 14 | object Fuse { 15 | def apply(u: scala.reflect.api.Universe)(ts: Traversal { val universe: u.type }*): TraversalFusion { val universe: u.type } = { 16 | assert(ts.nonEmpty, "Cannot fuse empty list of traversals") 17 | 18 | if (ts.exists(_.isInstanceOf[ScopingTraversal])) new ScopingTraversalFusion { 19 | val universe: u.type = u 20 | val traversals = ts.toSeq 21 | } 22 | else new TraversalFusion { 23 | val universe: u.type = u 24 | val traversals = ts.toSeq 25 | } 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /rules/core/src/main/scala/com/lightbend/abide/core/PolyImplicitOverload.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide._ 4 | import scala.tools.abide.traversal._ 5 | 6 | class PolyImplicitOverload(val context: Context) extends WarningRule { 7 | import context.universe._ 8 | 9 | val name = "poly-implicit-overload" 10 | 11 | case class Warning(val pos: Position) extends RuleWarning { 12 | val message: String = 13 | "Parametrized overloaded implicit methods are not visible as view bounds" 14 | } 15 | 16 | val step = optimize { 17 | case t @ Template(parents, self, body) => 18 | val clazz = t.tpe 19 | clazz.declarations filter (x => x.isImplicit && x.typeParams.nonEmpty) foreach { sym => 20 | // implicit classes leave both a module symbol and a method symbol as residue 21 | val alts = clazz.declaration(sym.name).alternatives filterNot (_.isModule) 22 | if (alts.size > 1) { 23 | nok(Warning(sym.pos)) 24 | } 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /rules/core/src/main/scala/com/lightbend/abide/core/PackageObjectClasses.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide._ 4 | import scala.tools.abide.traversal._ 5 | 6 | class PackageObjectClasses(val context: Context) extends WarningRule { 7 | import context.universe._ 8 | 9 | val name = "package-object-classes" 10 | 11 | case class Warning(val pos: Position, symbol: Symbol) extends RuleWarning { 12 | val message = 13 | s"""It is not recommended to define classes inside of package objects. 14 | |If possible, define ${symbol} in ${symbol.owner.owner} instead.""".stripMargin 15 | } 16 | 17 | def isPackageObjectMember(sym: Symbol) = 18 | sym.owner.isModuleClass && sym.owner.name == tpnme.PACKAGE && !sym.isSynthetic 19 | 20 | val step = optimize { 21 | case cd @ ClassDef(_, _, _, _) if isPackageObjectMember(cd.symbol) => 22 | nok(Warning(cd.pos, cd.symbol)) 23 | 24 | case md @ ModuleDef(_, _, _) if isPackageObjectMember(md.symbol) => 25 | nok(Warning(md.pos, md.symbol)) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /rules/core/src/main/scala/com/lightbend/abide/core/ValueClassSynchronized.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide._ 4 | import scala.tools.abide.traversal._ 5 | 6 | final class ValueClassSynchronized(val context: Context) extends PathRule { 7 | import context.universe._ 8 | import definitions.{ Object_synchronized, PredefModule } 9 | 10 | val name = "value-class-synchronized" 11 | type Element = Symbol 12 | 13 | case class Warning(ap: Apply) extends RuleWarning { 14 | val pos = ap.pos 15 | val message = "Within a user defined value class, `synchronized {}` locks `Predef`, which is unwise and probably unexpected" 16 | } 17 | 18 | private def enclosingClass = state.last.getOrElse(NoSymbol) 19 | 20 | val step = optimize { 21 | case tmpl: ImplDef => 22 | enter(tmpl.symbol) 23 | case ap @ Apply(TypeApply(sel @ Select(qual, _), _), _) => 24 | if (enclosingClass.isDerivedValueClass && sel.symbol == Object_synchronized && qual.symbol == PredefModule) 25 | nok(Warning(ap)) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /abide/src/main/scala/scala/tools/abide/traversal/WarningRule.scala: -------------------------------------------------------------------------------- 1 | package scala.tools.abide.traversal 2 | 3 | import scala.reflect.internal.traversal._ 4 | 5 | /** 6 | * WarningRule 7 | * 8 | * TraversalRule subtrait that provides the nok(warning : Warning) helper method to accumulate warnings. In this rule, 9 | * trees can be determined as invalid without requiring any extra context (ie. non-local information). 10 | * 11 | * Since verification is run after typer, such rules are either quite simple and purely stylistic (public/private considerations) or 12 | * rely on tree.symbol information to provides a certain non-locallity to tree information (like overrides, overloads). 13 | */ 14 | trait WarningRule extends TraversalRule { 15 | import context.universe._ 16 | 17 | def emptyState = State(Nil) 18 | case class State(warnings: List[Warning]) extends RuleState { 19 | def nok(warning: Warning): State = State(warning :: warnings) 20 | } 21 | 22 | /** Reports a warning */ 23 | def nok(warning: Warning): Unit = { transform(_ nok warning) } 24 | } 25 | -------------------------------------------------------------------------------- /rules/core/src/main/scala/com/lightbend/abide/core/RenamedDefaultParameter.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide._ 4 | import scala.tools.abide.traversal._ 5 | 6 | class RenamedDefaultParameter(val context: Context) extends WarningRule { 7 | import context.universe._ 8 | 9 | val name = "renamed-default-parameter" 10 | 11 | case class Warning(tree: Tree) extends RuleWarning { 12 | val pos = tree.pos 13 | val message = "Renaming parameters with default values can lead to unexpected behavior" 14 | } 15 | 16 | val step = optimize { 17 | case defDef: DefDef => 18 | defDef.symbol.overrides.foreach { overriden => 19 | val names = overriden.asMethod.paramLists.flatten.map(_.name).toSet 20 | (defDef.vparamss.flatten zip overriden.asMethod.paramLists.flatten).foreach { 21 | case (vd, o) => 22 | if (vd.symbol.isParamWithDefault && vd.symbol.name != o.name && names(vd.symbol.name)) { 23 | nok(Warning(vd)) 24 | } 25 | } 26 | } 27 | } 28 | } 29 | 30 | -------------------------------------------------------------------------------- /rules/extra/src/main/scala/com/typesafe/abide/extra/FixedNameOverrides.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.extra 2 | 3 | import scala.tools.abide._ 4 | import scala.tools.abide.traversal._ 5 | 6 | class FixedNameOverrides(val context: Context) extends WarningRule { 7 | import context.universe._ 8 | 9 | val name = "fixed-name-overrides" 10 | 11 | case class Warning(vd: ValDef, sn: String, sym: MethodSymbol) extends RuleWarning { 12 | val pos = vd.pos 13 | val message = s"Renaming parameter ${vd.name} of method ${vd.symbol.owner.name} from ${sn} in super-type ${sym.owner.name} can lead to confusion" 14 | } 15 | 16 | val step = optimize { 17 | case defDef: DefDef if !defDef.symbol.isSynthetic && !defDef.symbol.owner.isSynthetic => 18 | defDef.symbol.overrides.foreach { overriden => 19 | (defDef.vparamss.flatten zip overriden.asMethod.paramLists.flatten).foreach { 20 | case (vd, o) => 21 | if (vd.symbol.name != o.name) { 22 | nok(Warning(vd, o.name.toString, overriden.asMethod)) 23 | } 24 | } 25 | } 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /rules/core/src/main/scala/com/lightbend/abide/core/EmptyOrNonEmptyUsingSize.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide._ 4 | import scala.tools.abide.traversal._ 5 | 6 | class EmptyOrNonEmptyUsingSize(val context: Context) extends WarningRule { 7 | 8 | import context.universe._ 9 | 10 | val name = "empty-nonempty-using-size" 11 | 12 | case class Warning(appl: Tree, empty: Boolean) extends RuleWarning { 13 | val pos = appl.pos 14 | val message = "Traversable.size is very expensive on some collections, use " + 15 | (if (empty) "isEmpty" 16 | else "nonEmpty") + "which is O(1)" 17 | } 18 | 19 | val step = optimize { 20 | case a @ q"$subj.size == 0" if isTraversable(subj) => nok(Warning(a, empty = true)) 21 | case a @ q"$subj.length == 0" if isTraversable(subj) => nok(Warning(a, empty = true)) 22 | case a @ q"$subj.size != 0" if isTraversable(subj) => nok(Warning(a, empty = false)) 23 | case a @ q"$subj.length != 0" if isTraversable(subj) => nok(Warning(a, empty = false)) 24 | } 25 | 26 | private def isTraversable(tree: Tree) = tree.tpe <:< typeOf[Traversable[_]] 27 | 28 | } 29 | -------------------------------------------------------------------------------- /abide/src/main/scala/scala/tools/abide/presentation/Presenter.scala: -------------------------------------------------------------------------------- 1 | package scala.tools.abide.presentation 2 | 3 | import scala.tools.nsc._ 4 | import scala.tools.abide._ 5 | 6 | /** 7 | * Supertrait for presenter generator objects. 8 | * 9 | * The [[PresenterGenerator]] that will instantiate the [[Presenter]] necessary. 10 | * 11 | * 12 | * @see [[presentation.ConsolePresenterGenerator]] for a concrete example 13 | * @see [[Presenter]] 14 | */ 15 | trait PresenterGenerator { 16 | 17 | /** 18 | * Buils a new [[Presenter]] 19 | */ 20 | def getPresenter(global: Global): Presenter 21 | 22 | } 23 | 24 | /** 25 | * Presenter 26 | * 27 | * Base class for result "presentation". A presenter instance will receive warnings as input 28 | * from the analyzer in a given CompilationUnit and should use these to generate output. 29 | * 30 | * @see [[scala.tools.abide.Warning]] 31 | * @see [[ConsolePresenter]] for a concrete example 32 | */ 33 | trait Presenter { 34 | protected val global: Global 35 | import global._ 36 | 37 | /** Generate output from warnings produced by Abide analysis */ 38 | def apply(unit: CompilationUnit, warnings: List[Warning]): Unit 39 | } 40 | -------------------------------------------------------------------------------- /rules/core/src/main/scala/com/lightbend/abide/core/NullaryUnit.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide._ 4 | import scala.tools.abide.traversal._ 5 | 6 | class NullaryUnit(val context: Context) extends WarningRule { 7 | import context.universe._ 8 | 9 | val name = "nullary-unit" 10 | 11 | case class Warning(val pos: Position, name: Name) extends RuleWarning { 12 | val message = s"Side-effecting nullary methods are discouraged: try defining as `def $name()` instead" 13 | } 14 | 15 | // Don't warn for e.g. the implementation of a generic method being parameterized on Unit 16 | def isOk(sym: Symbol) = ( 17 | sym.isGetter 18 | || (sym.name containsName nme.DEFAULT_GETTER_STRING) 19 | || sym.allOverriddenSymbols.exists(over => !(over.tpe.resultType =:= sym.tpe.resultType)) 20 | ) 21 | 22 | def check(df: Tree) = df.symbol.tpe match { 23 | case NullaryMethodType(resttp) if resttp =:= typeOf[Unit] && !isOk(df.symbol) => 24 | nok(Warning(df.pos, df.symbol.name)) 25 | case _ => () 26 | } 27 | 28 | val step = optimize { 29 | case valDef @ ValDef(_, _, _, _) => check(valDef) 30 | case defDef @ DefDef(_, _, _, _, _, _) => check(defDef) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /wiki/extra-rules.md: -------------------------------------------------------------------------------- 1 | # Extra rules package [rules/extra](/rules/extra/src) 2 | 3 | The rules found in this package form a complement to the [rules/core](/wiki/core-rules.md) package and provides rules that are generally not as widely accepted as those found in core. One will typically not use all the rules provided by this package, but some may be of interest and can be selectively enabled when necessary. 4 | 5 | ## Fixing the overriden method parameter names 6 | 7 | name : **fixed-name-overrides** 8 | source : [FixedNameOverrides](/rules/extra/src/main/scala/com/lightbend/abide/extra/FixedNameOverrides.scala) 9 | 10 | When overriding a method, it can be worthwhile to keep the argument names (and ordering) to make sure the source remains clear and easily readable when traversing a hierarchy. This rule will enforce such consistent naming and provide warnings when the names differ on method override. 11 | 12 | ## Usage of isInstanceOf and asInstanceOf instead of pattern matching 13 | 14 | name : **instance-of-used** 15 | source : [InstanceOfUsed](/rules/extra/src/main/scala/com/lightbend/abide/extra/InstanceOfUsed.scala) 16 | 17 | It is safer and more idiomatic to use pattern matching than to use isInstanceOf and asInstanceOf on objects -------------------------------------------------------------------------------- /abide/src/main/scala/scala/tools/abide/Context.scala: -------------------------------------------------------------------------------- 1 | package scala.tools.abide 2 | 3 | import scala.reflect.internal._ 4 | 5 | /** 6 | * Base trait for context generator objects that provide rules with shared context. 7 | * 8 | * If some information requires heavy computing and can be shared between two or more rules, it may be 9 | * useful to cache the result in a common place. To enable this, build a new type that extends the [[Context]] 10 | * type with these capabilities and provide these rules with a companion object that extends [[ContextGenerator]]. 11 | * The abide framework will automatically load these objects and compute the lub of shared information for each 12 | * rule and instantiate the contexts accordingly. 13 | * 14 | * @see [[Context]] 15 | * @see com.lightbend.abide.sample.PublicMutable for a concrete example 16 | */ 17 | trait ContextGenerator { 18 | def getContext(universe: SymbolTable): Context 19 | } 20 | 21 | /** 22 | * Context base-class that lets rules share common information (like the compiler instance [[universe]]). 23 | * 24 | * More information can be added to the shared context through an extension and companion class mechanism described 25 | * in more detail in [[ContextGenerator]]. 26 | */ 27 | class Context(val universe: SymbolTable) 28 | 29 | -------------------------------------------------------------------------------- /wiki/akka-rules.md: -------------------------------------------------------------------------------- 1 | # Akka rules package [rules/akka](/rules/akka/src) 2 | 3 | These rules should apply to code that uses the Akka framework. The provided rules require the libraries 4 | ```scala 5 | "com.typesafe.akka" %% "akka-actor" % "2.3.3" 6 | "com.typesafe.akka" %% "akka-stream-experimental" % "0.4" 7 | ``` 8 | 9 | TODO: this requirement should be made more flexible by people who know more about Akka than I do! 10 | 11 | ## Sender method called in deferred block 12 | 13 | name : **sender-in-future** 14 | source : [SenderInFuture](/rules/akka/src/main/scala/com/lightbend/abide/akka/SenderInFuture.scala) 15 | 16 | The `sender()` method should _never_ be called inside of a code block that won't be executed immediately in the `receive` method since the resulting sender might not be the right one anymore when the deferred code is actually executed. For example, the code 17 | ```scala 18 | class ExampleActor extends Actor { 19 | def receive = { 20 | case "Hi" => future { 21 | sender() ! "Hello" 22 | } 23 | } 24 | } 25 | ``` 26 | should be written as 27 | ```scala 28 | class ExampleActor extends Actor { 29 | def receive = { 30 | case "Hi" => 31 | val s = sender() 32 | future { s ! "Hello" } 33 | } 34 | } 35 | ``` 36 | to make sure the sender we use to reply is indeed the one associated to the received message. 37 | -------------------------------------------------------------------------------- /rules/core/src/main/scala/com/lightbend/abide/core/PrivateShadow.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide._ 4 | import scala.tools.abide.traversal._ 5 | 6 | // https://issues.scala-lang.org/browse/SI-4762 7 | class PrivateShadow(val context: Context) extends WarningRule { 8 | import context.universe._ 9 | 10 | val name = "private-shadow" 11 | 12 | case class Warning(val pos: Position, sym: Symbol, m2: Symbol) extends RuleWarning { 13 | val message: String = s"${sym.accessString} ${sym.fullLocationString} shadows mutable $m2 inherited from ${m2.owner}. Changes to $m2 will not be visible within ${sym.owner} - you may want to give them distinct names." 14 | } 15 | 16 | val step = optimize { 17 | case sel @ Select(qual @ This(_), _) => 18 | val sym = sel.symbol 19 | if (sym.isPrivateThis && 20 | (!sym.isMethod || sym.asMethod.paramss.isEmpty) && 21 | qual.symbol.isClass) { 22 | 23 | qual.symbol.asClass.baseClasses.drop(1) foreach { parent => 24 | parent.typeSignature.declarations.filterNot(x => x.isPrivate || x.isLocalToThis) foreach { m2 => 25 | if (sym.name == m2.name && 26 | m2.isMethod && 27 | m2.asMethod.isGetter && 28 | m2.asMethod.accessed.isVar) { 29 | 30 | nok(Warning(sel.pos, sym, m2)) 31 | } 32 | } 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /rules/core/src/test/scala/com/lightbend/abide/core/StupidRecursionTest.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide.traversal._ 4 | import com.lightbend.abide.core._ 5 | 6 | class StupidRecursionTest extends TraversalTest { 7 | 8 | val rule = new StupidRecursion(context) 9 | 10 | "Definitions without parameters" should "not be stupidly defined as themselves" in { 11 | val tree = fromString(""" 12 | class Toto { 13 | def test: Int = test 14 | } 15 | """) 16 | 17 | global.ask { () => 18 | val syms = apply(rule)(tree).map(_.tree.symbol.toString) 19 | syms.sorted should be(List("method test")) 20 | } 21 | } 22 | 23 | it should "should also be identified in local methods" in { 24 | val tree = fromString(""" 25 | class Toto { 26 | def test : Int = { 27 | def rec : Int = rec 28 | rec 29 | } 30 | } 31 | """) 32 | 33 | global.ask { () => 34 | val syms = apply(rule)(tree).map(_.tree.symbol.toString) 35 | syms.sorted should be(List("method rec")) 36 | } 37 | } 38 | 39 | it should "not be identified in non-direct children" in { 40 | val tree = fromString(""" 41 | class Toto(val a : Int) { 42 | trait Titi { 43 | val a : Int 44 | } 45 | 46 | val titi = new Titi { 47 | val a = Toto.this.a 48 | } 49 | } 50 | """) 51 | 52 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /project/Formatting.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import Keys._ 3 | import com.typesafe.sbt.SbtScalariform.{ ScalariformKeys => sr, _ } 4 | 5 | // taken from sbt's build (modulo formatter preferences) 6 | object Formatting { 7 | lazy val BuildConfig = config("build") extend Compile 8 | lazy val BuildSbtConfig = config("buildsbt") extend Compile 9 | 10 | lazy val settings: Seq[Setting[_]] = Seq() ++ scalariformSettings ++ prefs 11 | lazy val prefs: Seq[Setting[_]] = { 12 | import scalariform.formatter.preferences._ 13 | Seq(sr.preferences := sr.preferences.value 14 | .setPreference(AlignSingleLineCaseStatements, true) 15 | .setPreference(CompactControlReadability, true) 16 | .setPreference(DanglingCloseParenthesis, Preserve) 17 | ) 18 | } 19 | lazy val sbtFilesSettings: Seq[Setting[_]] = Seq() ++ scalariformSettings ++ prefs ++ 20 | inConfig(BuildConfig)(configScalariformSettings) ++ 21 | inConfig(BuildSbtConfig)(configScalariformSettings) ++ 22 | Seq( 23 | scalaSource in BuildConfig := baseDirectory.value / "project", 24 | scalaSource in BuildSbtConfig := baseDirectory.value / "project", 25 | includeFilter in (BuildConfig, sr.format) := ("*.scala": FileFilter), 26 | includeFilter in (BuildSbtConfig, sr.format) := ("*.sbt": FileFilter), 27 | sr.format in Compile := { 28 | val x = (sr.format in BuildSbtConfig).value 29 | val y = (sr.format in BuildConfig).value 30 | (sr.format in Compile).value 31 | } 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /rules/core/src/main/scala/com/lightbend/abide/core/MatchCaseOnSeq.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide._ 4 | import scala.tools.abide.traversal._ 5 | 6 | class MatchCaseOnSeq(val context: Context) extends WarningRule { 7 | import context.universe._ 8 | 9 | val name = "match-case-on-seq" 10 | 11 | case class Warning(scrut: Tree, mtch: Tree) extends RuleWarning { 12 | val pos = mtch.pos 13 | val message = s"Seq typed scrutinee $scrut shouldn't be matched against :: typed case $mtch" 14 | } 15 | 16 | lazy val seqSymbol = rootMirror.getClassByName(TypeName("scala.collection.Seq")) 17 | lazy val immutableSeqSymbol = rootMirror.getClassByName(TypeName("scala.collection.immutable.Seq")) 18 | def isSeq(sym: Symbol): Boolean = sym == seqSymbol || sym == immutableSeqSymbol 19 | 20 | lazy val consSymbol = rootMirror.getClassByName(TypeName("scala.collection.immutable.$colon$colon")) 21 | def isCons(sym: Symbol): Boolean = sym == consSymbol 22 | 23 | val step = optimize { 24 | case q"$scrut match { case ..$cases }" if scrut.tpe != null && isSeq(scrut.tpe.typeSymbol) => 25 | val patterns = cases.collect { 26 | case cq"$bind @ $pat if $guard => $expr" => pat 27 | case cq"$pat if $guard => $expr" => pat 28 | } 29 | 30 | patterns.foreach(pat => pat match { 31 | case q"$id(..$args)" if id.tpe != null && isCons(id.tpe.resultType.typeSymbol) => 32 | nok(Warning(scrut, pat)) 33 | case _ => 34 | }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /rules/core/src/test/scala/com/lightbend/abide/core/PackageObjectClassesTest.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide.traversal._ 4 | import com.lightbend.abide.core._ 5 | 6 | class PackageObjectClassesTest extends TraversalTest { 7 | 8 | val rule = new PackageObjectClasses(context) 9 | 10 | "Class definitions" should "not be valid if in a package object" in { 11 | val tree = fromString(""" 12 | package object test { 13 | class A 14 | case class B() 15 | private class C { def x: Int = 3 } 16 | } 17 | """) 18 | 19 | global.ask { () => apply(rule)(tree).size should be(3) } 20 | } 21 | 22 | it should "be valid if not in a package object" in { 23 | val tree = fromString(""" 24 | package test2 { 25 | class A 26 | case class B() 27 | private class C { def x: Int = 3 } 28 | } 29 | """) 30 | 31 | global.ask { () => apply(rule)(tree) shouldBe empty } 32 | } 33 | 34 | "Object definitions" should "not be valid if in a pacakge object" in { 35 | val tree = fromString(""" 36 | package object test { 37 | object D 38 | object E 39 | } 40 | """) 41 | 42 | global.ask { () => apply(rule)(tree).size should be(2) } 43 | } 44 | 45 | it should "be valid if not in a package object" in { 46 | val tree = fromString(""" 47 | package test2 { 48 | object D 49 | } 50 | """) 51 | 52 | global.ask { () => apply(rule)(tree) shouldBe empty } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /rules/core/src/test/scala/com/lightbend/abide/core/ValueClassSynchronizedTest.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide.traversal._ 4 | import com.lightbend.abide.core._ 5 | 6 | class ValueClassSynchronizedTest extends TraversalTest { 7 | 8 | val rule = new ValueClassSynchronized(context) 9 | 10 | it should "not be valid when accidentally calling Predef.synchronized" in { 11 | val tree = fromString(""" 12 | class C(val self: AnyRef) extends AnyVal { 13 | def foo = synchronized { toString } 14 | } 15 | """) 16 | 17 | global.ask { () => apply(rule)(tree).size should be(1) } 18 | } 19 | 20 | it should "be valid calling some other synchronized" in { 21 | val tree = fromString(""" 22 | class C(val self: AnyRef) { 23 | def foo = self.synchronized { toString } 24 | } 25 | """) 26 | 27 | global.ask { () => apply(rule)(tree) shouldBe empty } 28 | } 29 | 30 | it should "be valid outside of value class" in { 31 | val tree = fromString(""" 32 | class C { 33 | def foo = Predef.synchronized { toString } 34 | } 35 | """) 36 | 37 | global.ask { () => apply(rule)(tree) shouldBe empty } 38 | } 39 | 40 | it should "be valid in class nested in value class" in { 41 | val tree = fromString(""" 42 | class C(val self: AnyRef) { 43 | def foo = { new { def foo = synchronized { toString } } } 44 | } 45 | """) 46 | 47 | global.ask { () => apply(rule)(tree) shouldBe empty } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /rules/core/src/main/scala/com/lightbend/abide/core/InferAny.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide._ 4 | import scala.tools.abide.traversal._ 5 | 6 | class InferAny(val context: Context) extends PathRule { 7 | import context.universe._ 8 | 9 | val name = "infer-any" 10 | 11 | case class Warning(app: Tree, tpt: Tree) extends RuleWarning { 12 | val pos = app.pos 13 | val message = s"A type was inferred to be `${tpt.tpe.typeSymbol.name}`. This may indicate a programming error." 14 | } 15 | 16 | // Use PathRule to track whether the traversal is currently inside a synthetic method 17 | // (for example a method generated for a case class) which should be ignored. 18 | type Element = Unit 19 | def inSyntheticMethod = state.last.nonEmpty 20 | def enterSyntheticMethod() = enter(()) 21 | 22 | def containsAny(t: Type) = 23 | t.contains(typeOf[Any].typeSymbol) || t.contains(typeOf[AnyVal].typeSymbol) 24 | 25 | def isInferredAny(tree: Tree) = tree match { 26 | case tpt @ TypeTree() => tpt.original == null && containsAny(tpt.tpe) 27 | case _ => false 28 | } 29 | 30 | val step = optimize { 31 | case df @ DefDef(_, _, _, _, _, _) if df.symbol.isSynthetic => 32 | enterSyntheticMethod() 33 | 34 | case app @ Apply(TypeApply(fun, targs), args) if targs.exists(isInferredAny) && !inSyntheticMethod => 35 | val existsExplicitAny = args.map(_.tpe).exists(containsAny(_)) 36 | if (!existsExplicitAny) { 37 | nok(Warning(app, targs.find(isInferredAny).get)) 38 | } 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014-2016, Lightbend Inc. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the Abide project nor the names of its contributors 15 | may be used to endorse or promote products derived from this 16 | software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /rules/core/src/main/scala/com/lightbend/abide/core/ValInsteadOfVar.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide._ 4 | import scala.tools.abide.traversal._ 5 | 6 | trait ValInsteadOfVar extends ExistentialRule { 7 | type Key = context.universe.Symbol 8 | } 9 | 10 | class LocalValInsteadOfVar(val context: Context) extends ValInsteadOfVar { 11 | import context.universe._ 12 | 13 | val name = "local-val-instead-of-var" 14 | 15 | case class Warning(tree: Tree) extends RuleWarning { 16 | val pos = tree.pos 17 | val message = s"The `var` $tree was never assigned locally and should therefore be declared as a `val`" 18 | } 19 | 20 | val step = optimize { 21 | case varDef @ q"$mods var $name : $tpt = $value" if varDef.symbol.owner.isMethod => 22 | nok(varDef.symbol, Warning(varDef)) 23 | case q"$rcv = $expr" => 24 | ok(rcv.symbol) 25 | } 26 | } 27 | 28 | class MemberValInsteadOfVar(val context: Context) extends ValInsteadOfVar { 29 | import context.universe._ 30 | 31 | val name = "member-val-instead-of-var" 32 | 33 | case class Warning(tree: Tree) extends RuleWarning { 34 | val pos = tree.pos 35 | val message = s"The member `var` $tree was never assigned locally and should therefore be declared as a `val`" 36 | } 37 | 38 | val step = optimize { 39 | case varDef @ q"$mods var $name : $tpt = $value" => 40 | val setter: Symbol = varDef.symbol.setter 41 | if (setter.isPrivate) nok(varDef.symbol, Warning(varDef)) 42 | case set @ q"$setter(..$args)" if setter.symbol.isSetter => 43 | ok(setter.symbol.accessed) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /macros/src/main/scala/scala/reflect/internal/traversal/ScopingTraversalFusion.scala: -------------------------------------------------------------------------------- 1 | package scala.reflect.internal.traversal 2 | 3 | /** 4 | * ScopingTraversalFusion 5 | * 6 | * Extension of [[TraversalFusion]] that supports [[ScopingTraversal]] traversals as well as 7 | * standard [[Traversal]] traversals. The scoping traversals are handled by tracking `leaver` 8 | * functions for each traversal which are applied as transformations (@see [[Traversal.transform]]) once 9 | * the traversal leaves a certain tree node. 10 | * 11 | * @see [[ScopingTraversal]] 12 | */ 13 | trait ScopingTraversalFusion extends TraversalFusion { 14 | import universe._ 15 | 16 | private type Leaver = (Traversal, (Nothing => Any)) 17 | 18 | private def foreach(tree: Tree)(enter: Tree => List[Leaver]): Unit = { 19 | def rec(tree: Tree): Unit = { 20 | val leavers = enter(tree) 21 | tree.children.foreach(rec(_)) 22 | leavers.foreach { 23 | case (traversal, leaver) => 24 | traversal.transform(leaver.asInstanceOf[traversal.State => traversal.State]) 25 | } 26 | } 27 | 28 | rec(tree) 29 | } 30 | 31 | override def traverse(tree: Tree): Unit = { 32 | traversals.foreach(_.init) 33 | 34 | def enter(tree: Tree): List[Leaver] = getTraversals(tree).flatMap { traversal => 35 | val defined = traversal.apply(tree) 36 | 37 | val leaver: Option[Nothing => Any] = if (!defined) None else traversal match { 38 | case scoper: ScopingTraversal => scoper.consumeLeaver 39 | case _ => None 40 | } 41 | 42 | leaver.map(l => traversal -> l) 43 | } 44 | 45 | foreach(tree)(enter) 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /rules/akka/src/main/scala/com/typesafe/abide/akka/SenderInFuture.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.akka 2 | 3 | import scala.tools.abide._ 4 | import scala.tools.abide.traversal._ 5 | 6 | class SenderInFuture(val context: Context) extends PathRule { 7 | import context.universe._ 8 | 9 | val name = "sender-in-future" 10 | 11 | case class Warning(tree: Tree) extends RuleWarning { 12 | val pos = tree.pos 13 | val message = s"sender() is not stable and should not be accessed in non-thread-safe environments" 14 | } 15 | 16 | sealed abstract class Element 17 | case class Receive(sym: Symbol) extends Element 18 | case object Future extends Element 19 | 20 | lazy val actorSym = rootMirror.getClassByName(TypeName("akka.actor.Actor")) 21 | 22 | lazy val receiveSym = actorSym.toType.members.find(_.name == TermName("receive")).get 23 | lazy val senderSym = actorSym.toType.members.find(_.name == TermName("sender")).get 24 | 25 | lazy val futureSym = { 26 | val concurrentSym = rootMirror.getModuleByName(TermName("scala.concurrent")) 27 | concurrentSym.moduleClass.toType.members.find(_.name == TermName("future")).get 28 | } 29 | 30 | val step = optimize { 31 | case dd @ q"def receive : $tpt = $rhs" if dd.symbol.overrides.exists(_ == receiveSym) => 32 | enter(Receive(dd.symbol.owner)) 33 | case q"$caller(..$args)" if caller.symbol == futureSym => state.last match { 34 | case Some(Receive(sym)) => enter(Future) 35 | case _ => 36 | } 37 | case tree @ Apply(sender @ Select(actor, TermName("sender")), _) if sender.symbol == senderSym => 38 | if (state matches (Receive(actor.symbol), Future)) { 39 | nok(Warning(tree)) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /rules/core/src/test/scala/com/lightbend/abide/core/MemberValInsteadOfVarTest.scala: -------------------------------------------------------------------------------- 1 | package scala.tools.abide.core 2 | 3 | import scala.tools.abide.traversal._ 4 | import com.lightbend.abide.core._ 5 | 6 | class MemberValInsteadOfVarTest extends TraversalTest { 7 | 8 | val rule = new MemberValInsteadOfVar(context) 9 | 10 | "Member local vars" should "be vals when not assigned" in { 11 | val tree = fromString(""" 12 | class Toto { 13 | private var a = 0 14 | def test(i: Int) : Int = a + i 15 | } 16 | """) 17 | 18 | global.ask { () => 19 | val syms = apply(rule)(tree).map(_.tree.symbol.toString) 20 | syms.sorted should be(List("variable a")) 21 | } 22 | } 23 | 24 | it should "be vars when assigned" in { 25 | val tree = fromString(""" 26 | class Toto { 27 | private var a = 0 28 | def test(i : Int) : Unit = { a = i } 29 | def test2(i : Int) : Int = a + i 30 | } 31 | """) 32 | 33 | global.ask { () => 34 | apply(rule)(tree).isEmpty should be(true) 35 | } 36 | } 37 | 38 | it should "be vars when assignment predates declaration" in { 39 | val tree = fromString(""" 40 | class Toto { 41 | def test(i : Int) : Unit = { a = i } 42 | private var a = 0 43 | } 44 | """) 45 | 46 | global.ask { () => 47 | apply(rule)(tree).isEmpty should be(true) 48 | } 49 | } 50 | 51 | it should "be ignored when var isn't private" in { 52 | val tree = fromString(""" 53 | class Toto { 54 | protected var a = 0 55 | var b = 0 56 | } 57 | """) 58 | 59 | global.ask { () => 60 | apply(rule)(tree).isEmpty should be(true) 61 | } 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /abide/src/main/scala/scala/tools/abide/traversal/ScopingRule.scala: -------------------------------------------------------------------------------- 1 | package scala.tools.abide.traversal 2 | 3 | import scala.reflect.internal.traversal._ 4 | 5 | /** 6 | * ScopingRule 7 | * 8 | * TraversalRule subtrait that provides helper methods to manage scoping during traversal. Then [[enter]] method 9 | * will push the current tree to the scope and register a leaver method that will pop it once we leave that tree. 10 | * 11 | * As in [[WarningRule]], warnings are determined given local (and scoping) context in a single pass (no 12 | * validation/invalidation mechanism). 13 | */ 14 | trait ScopingRule extends TraversalRule with ScopingTraversal { 15 | import context.universe._ 16 | 17 | /** Scoping type (eg. method symbol, class symbol, etc.) */ 18 | type Owner 19 | 20 | def emptyState = State(Nil, Nil) 21 | case class State(scope: List[Owner], warnings: List[Warning]) extends RuleState { 22 | def nok(warning: Warning): State = State(scope, warning :: warnings) 23 | def enter(owner: Owner): State = State(owner :: scope, warnings) 24 | def leave: State = State(scope.tail, warnings) 25 | 26 | def childOf(matches: Owner => Boolean): Boolean = scope.nonEmpty && matches(scope.head) 27 | def childOf(owner: Owner): Boolean = childOf(_ == owner) 28 | 29 | def in(matches: Owner => Boolean): Boolean = scope.exists(matches(_)) 30 | 31 | def parents(matches: Owner => Boolean): List[Owner] = scope.filter(matches(_)) 32 | def parent: Option[Owner] = scope.headOption 33 | } 34 | 35 | /** Register owner as current scope (pushes it onto scoping stack) */ 36 | def enter(owner: Owner): Unit = { transform(_ enter owner, _.leave) } 37 | 38 | /** Reports a warning */ 39 | def nok(warning: Warning): Unit = { transform(_ nok warning) } 40 | } 41 | -------------------------------------------------------------------------------- /macros/src/test/scala/scala/reflect/internal/traversal/OrderingTest.scala: -------------------------------------------------------------------------------- 1 | package scala.reflect.internal.traversal 2 | 3 | import scala.reflect.internal.traversal._ 4 | import org.scalatest._ 5 | 6 | class OrderingTest extends FlatSpec with Matchers with TreeProvider { 7 | import global._ 8 | 9 | object stackingTraverser extends { 10 | val universe: OrderingTest.this.global.type = OrderingTest.this.global 11 | } with ScopingTraversal { 12 | import universe._ 13 | 14 | type State = List[Tree] 15 | def emptyState: State = Nil 16 | 17 | val step: PartialFunction[Tree, Unit] = { 18 | case tree => transform(tree :: _, state => state match { 19 | case x :: xs if x == tree => xs 20 | case _ => state 21 | }) 22 | } 23 | } 24 | 25 | "Traversal ordering" should "be valid in trivial code" in { 26 | val tree = fromString(""" 27 | package toto 28 | class Toto { 29 | private var a = 10 30 | def toto(i : Int) : Int = { 31 | a += 1 32 | a + i 33 | } 34 | } 35 | """) 36 | 37 | global.ask { () => 38 | stackingTraverser.traverse(tree) 39 | stackingTraverser.result.isEmpty should be(true) 40 | } 41 | } 42 | 43 | it should "and in non-trivial code (AddressBook.scala)" in { 44 | val tree = fromFile("traversal/AddressBook.scala") 45 | global.ask { () => 46 | stackingTraverser.traverse(tree) 47 | stackingTraverser.result.isEmpty should be(true) 48 | } 49 | } 50 | 51 | it should "hold in non-trivial code (SimpleInterpreter.scala)" in { 52 | val tree = fromFile("traversal/SimpleInterpreter.scala") 53 | global.ask { () => 54 | stackingTraverser.traverse(tree) 55 | stackingTraverser.result.isEmpty should be(true) 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /abide/src/main/scala/scala/tools/abide/Abide.scala: -------------------------------------------------------------------------------- 1 | package scala.tools.abide 2 | 3 | import scala.tools.nsc._ 4 | import scala.tools.nsc.plugins._ 5 | import scala.tools.nsc.reporters._ 6 | import scala.reflect.runtime.{ universe => ru } 7 | import scala.reflect.internal.util._ 8 | 9 | /** Main class provider for the Abide framework. Useful for creating build-tool plugins. */ 10 | object Abide { 11 | 12 | /** Simple compiler that stops after `typer` and registers the Abide compiler plugin ([[compiler.AbidePlugin]]) */ 13 | class AbideCompiler(settings: Settings, reporter: Reporter) extends Global(settings, reporter) { 14 | lazy val abidePlugin = new scala.tools.abide.compiler.AbidePlugin(this) 15 | 16 | override protected def loadRoughPluginsList: List[Plugin] = { 17 | abidePlugin :: super.loadRoughPluginsList 18 | } 19 | 20 | override protected def computeInternalPhases(): Unit = { 21 | val phs = List( 22 | syntaxAnalyzer -> "parse source into ASTs, perform simple desugaring", 23 | analyzer.namerFactory -> "resolve names, attach symbols to named trees", 24 | analyzer.packageObjects -> "load package objects", 25 | analyzer.typerFactory -> "the meat and potatoes: type the trees" 26 | ) 27 | 28 | phs foreach { phasesSet += _._1 } 29 | } 30 | } 31 | 32 | private lazy val settings = new Settings(println) 33 | 34 | private lazy val reporter = new ConsoleReporter(settings) 35 | 36 | private lazy val compiler = new AbideCompiler(settings, reporter) 37 | 38 | /** Main method for the Abide framework, transparently passes all arguments to the [[AbideCompiler]] instance */ 39 | def main(args: Array[String]): Unit = { 40 | val command = new CompilerCommand(args.toList, settings) 41 | val run = new compiler.Run 42 | run.compile(command.files) 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /macros/src/test/resources/traversal/AddressBook.scala: -------------------------------------------------------------------------------- 1 | object addressbook { 2 | 3 | case class Person(name: String, age: Int) 4 | 5 | /** An AddressBook takes a variable number of arguments 6 | * which are accessed as a Sequence 7 | */ 8 | class AddressBook(a: Person*) { 9 | private val people: List[Person] = a.toList 10 | 11 | /** Serialize to XHTML. Scala supports XML literals 12 | * which may contain Scala expressions between braces, 13 | * which are replaced by their evaluation 14 | */ 15 | def toXHTML = 16 | 17 | 18 | 19 | 20 | 21 | { for (p <- people) yield 22 | 23 | 24 | 25 | 26 | } 27 |
NameAge
{ p.name } { p.age.toString() }
; 28 | } 29 | 30 | /** We introduce CSS using raw strings (between triple 31 | * quotes). Raw strings may contain newlines and special 32 | * characters (like \) are not interpreted. 33 | */ 34 | val header = 35 | 36 | 37 | { "My Address Book" } 38 | 39 | 45 | ; 46 | 47 | val people = new AddressBook( 48 | Person("Tom", 20), 49 | Person("Bob", 22), 50 | Person("James", 19)); 51 | 52 | val page = 53 | 54 | { header } 55 | 56 | { people.toXHTML } 57 | 58 | ; 59 | 60 | def main(args: Array[String]) { 61 | println(page) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /macros/src/test/scala/scala/reflect/internal/traversal/CompilerProvider.scala: -------------------------------------------------------------------------------- 1 | package scala.reflect.internal.traversal 2 | 3 | import scala.tools.nsc.io._ 4 | import scala.tools.nsc.interactive._ 5 | import scala.tools.nsc.reporters._ 6 | 7 | import scala.reflect.internal.util._ 8 | 9 | /** 10 | * trait CompilerProvider 11 | * 12 | * Trait that provides a simple presentation compiler, mostly used for writing tests. 13 | */ 14 | trait CompilerProvider { 15 | 16 | def silent: Boolean = false 17 | 18 | lazy val global: Global = { 19 | 20 | def urls(classLoader: java.lang.ClassLoader): List[String] = classLoader match { 21 | case cl: java.net.URLClassLoader => cl.getURLs.toList.map(_.toString) 22 | case c if c.getParent() != null => urls(c.getParent()) 23 | case c => scala.sys.error("invalid classloader") 24 | } 25 | 26 | val classpath = urls(java.lang.Thread.currentThread.getContextClassLoader) 27 | 28 | val settings = new scala.tools.nsc.Settings 29 | settings.usejavacp.value = false 30 | classpath.distinct.foreach { source => 31 | settings.classpath.append(source) 32 | settings.bootclasspath.append(source) 33 | } 34 | 35 | val compiler = new Global(settings, if (silent) { 36 | new Reporter { 37 | def info0(pos: Position, msg: String, severity: Severity, force: Boolean) = () 38 | } 39 | } 40 | else { 41 | new ConsoleReporter(settings) 42 | }) 43 | 44 | try { 45 | compiler.ask { () => 46 | new compiler.Run 47 | } 48 | } 49 | catch { 50 | case e: scala.reflect.internal.MissingRequirementError => 51 | val msg = s"""Could not initialize the compiler! 52 | | ${settings.userSetSettings.mkString("\n ")} 53 | | ${settings.classpath} 54 | | ${settings.bootclasspath} 55 | | ${settings.javabootclasspath}""".stripMargin 56 | throw new Exception(msg, e) 57 | } 58 | 59 | compiler 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /rules/core/src/test/scala/com/lightbend/abide/core/NullaryUnitTest.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide.traversal._ 4 | import com.lightbend.abide.core._ 5 | 6 | class NullaryUnitTest extends TraversalTest { 7 | 8 | val rule = new NullaryUnit(context) 9 | 10 | "Nullary methods" should "be valid when their return type is not unit" in { 11 | val tree = fromString(""" 12 | class Test { 13 | def nullary1 = 1 14 | def nullary2 = "2" 15 | def nullary3 = '3' 16 | } 17 | """) 18 | 19 | global.ask { () => apply(rule)(tree) shouldBe empty } 20 | } 21 | 22 | it should "not be valid when they return unit" in { 23 | val tree = fromString(""" 24 | class Test { 25 | def unit = () 26 | } 27 | """) 28 | 29 | global.ask { () => apply(rule)(tree).size should be(1) } 30 | } 31 | 32 | it should "not be valid when their return type is unit" in { 33 | val tree = fromString(""" 34 | class Test { 35 | def nullary: Unit = 23 36 | def notNullary: Int = 23 37 | } 38 | """) 39 | 40 | global.ask { () => apply(rule)(tree).size should be(1) } 41 | } 42 | 43 | it should "be valid if they have type parameters" in { 44 | val tree = fromString(""" 45 | class Test { 46 | def nullaryP[T]: Unit = () 47 | def nullaryP2[T, U] = println("hello") 48 | } 49 | """) 50 | 51 | global.ask { () => apply(rule)(tree) shouldBe empty } 52 | } 53 | 54 | it should "not be valid for each nullary method returning unit" in { 55 | val tree = fromString(""" 56 | class Test { 57 | private def nullaryUnit = () 58 | def nullaryNotUnit = 23 59 | def nonNullaryUnit() = () 60 | def paramNullary[T] = () 61 | var x = 1 62 | def moreComplicatedNullary = { 63 | println("start") 64 | x = x * 2 65 | } 66 | } 67 | """) 68 | 69 | global.ask { () => apply(rule)(tree).size should be(2) } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /rules/core/src/test/scala/com/lightbend/abide/core/LocalValInsteadOfVarTest.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide.traversal._ 4 | import com.lightbend.abide.core._ 5 | 6 | class LocalValInsteadOfVarTest extends TraversalTest { 7 | 8 | val rule = new LocalValInsteadOfVar(context) 9 | 10 | "Definition local vars" should "be vals when not assigned" in { 11 | val tree = fromString(""" 12 | class Toto { 13 | def test(a : Int) = { 14 | var b : Int = 2 15 | var c = 2 16 | a + b 17 | } 18 | } 19 | """) 20 | 21 | global.ask { () => 22 | val syms = apply(rule)(tree).map(_.tree.symbol.toString) 23 | syms.sorted should be(List("variable b", "variable c")) 24 | } 25 | } 26 | 27 | it should "be vars when assigned" in { 28 | val tree = fromString(""" 29 | class Toto { 30 | def test(a : Int) = { 31 | var b = 0 32 | b = a 33 | b 34 | } 35 | } 36 | """) 37 | 38 | global.ask { () => 39 | apply(rule)(tree).isEmpty should be(true) 40 | } 41 | } 42 | 43 | it should "be vars when assigned through special op" in { 44 | val tree = fromString(""" 45 | class Toto { 46 | def test(a : Int) = { 47 | var b = 0 48 | b += a 49 | b 50 | } 51 | } 52 | """) 53 | 54 | global.ask { () => 55 | apply(rule)(tree).isEmpty should be(true) 56 | } 57 | } 58 | 59 | it should "not show up in member analysis" in { 60 | val tree = fromString(""" 61 | class Toto { 62 | private var a = 0 63 | } 64 | """) 65 | 66 | global.ask { () => 67 | apply(rule)(tree).isEmpty should be(true) 68 | } 69 | 70 | val tree2 = fromString(""" 71 | class Toto { 72 | private var a = 0 73 | def test() { 74 | a = 2 75 | } 76 | } 77 | """) 78 | 79 | global.ask { () => 80 | apply(rule)(tree2).isEmpty should be(true) 81 | } 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /abide/src/main/scala/scala/tools/abide/traversal/ExistentialRule.scala: -------------------------------------------------------------------------------- 1 | package scala.tools.abide.traversal 2 | 3 | import scala.reflect.internal.traversal._ 4 | 5 | /** 6 | * ExistentialRule 7 | * 8 | * TraversalRule subtrait that provides helper methods to mark code constructs as invalid and then check for 9 | * existence of certain validators that will mark the issue as resolved. The nok(key : Key, warning : Warning) method 10 | * will mark the key as possibly invalid and associate warning to that concern, whereas the ok(key : Key) method 11 | * will mark the key as valid and any issue pertaining to that key is resolved. 12 | * 13 | * Since traversal ordering doesn't necessarily correlate with control-flow, one _cannot_ make assumptions on 14 | * validation/invalidation ordering. The rule definition _must_ be agnostic to which is discovered first. In this case, 15 | * validation is allways stronger than invalidation. In other words, once ok has been called on a key, no warning can stem from that key, 16 | * regardless of how many noks have taken place before/after. 17 | */ 18 | trait ExistentialRule extends TraversalRule { 19 | import context.universe._ 20 | 21 | /** Warning key type that uniquely defines the source of a warning */ 22 | type Key 23 | 24 | def emptyState = State(Map.empty) 25 | case class State(map: Map[Key, Option[Warning]]) extends RuleState { 26 | def warnings = map.flatMap(_._2).toList 27 | def ok(key: Key): State = State(map + (key -> None)) 28 | def nok(key: Key, warning: Warning): State = State(map + (key -> map.getOrElse(key, Some(warning)))) 29 | } 30 | 31 | /** Marks a key as valid forever */ 32 | def ok(key: Key): Unit = { transform(_ ok key) } 33 | 34 | /** 35 | * Marks a key as possibly invalid until an ok is found or traversal ends (in which case the key is considered as globally invalid). 36 | * If such a state is reached, the warning that was assigned to the key is considered a valid warning that should be reported. 37 | */ 38 | def nok(key: Key, warning: Warning): Unit = { transform(_ nok (key, warning)) } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /rules/akka/src/test/scala/com/typesafe/abide/akka/SenderInFutureTest.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.akka 2 | 3 | import scala.tools.abide.traversal._ 4 | import com.lightbend.abide.akka._ 5 | 6 | class SenderInFutureTest extends TraversalTest { 7 | 8 | val rule = new SenderInFuture(context) 9 | 10 | "Stable sender access" should "be valid out of futures" in { 11 | val tree = fromString(""" 12 | class Titi extends akka.actor.Actor { 13 | def receive = { 14 | case _ => 15 | val s = sender() 16 | s ! "Hello" 17 | } 18 | } 19 | """) 20 | 21 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 22 | } 23 | 24 | it should "be valid inside of futures" in { 25 | val tree = fromString(""" 26 | import scala.concurrent._ 27 | import scala.concurrent.ExecutionContext.Implicits.global 28 | class Titi extends akka.actor.Actor { 29 | def receive = { 30 | case _ => 31 | val s = sender() 32 | future { 33 | s ! "Hello" 34 | } 35 | } 36 | } 37 | """) 38 | 39 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 40 | } 41 | 42 | "Unstable sender access" should "be valid out of futures" in { 43 | val tree = fromString(""" 44 | class Titi extends akka.actor.Actor { 45 | def receive = { 46 | case _ => 47 | sender() ! "Hello" 48 | } 49 | } 50 | """) 51 | 52 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 53 | } 54 | 55 | it should "be invalid inside of futures" in { 56 | val tree = fromString(""" 57 | import scala.concurrent._ 58 | import scala.concurrent.ExecutionContext.Implicits.global 59 | class Titi extends akka.actor.Actor { 60 | def receive = { 61 | case _ => 62 | future { 63 | sender() ! "Hello" 64 | } 65 | } 66 | } 67 | """) 68 | 69 | global.ask { () => apply(rule)(tree).isEmpty should be(false) } 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /rules/core/src/test/scala/com/lightbend/abide/core/MatchCaseOnSeqTest.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide.traversal._ 4 | import com.lightbend.abide.core._ 5 | 6 | class MatchCaseOnSeqTest extends TraversalTest { 7 | 8 | val rule = new MatchCaseOnSeq(context) 9 | 10 | "Seqs" should "not be matched with ::" in { 11 | val tree = fromString(""" 12 | class Toto { 13 | def test(list : Seq[Int]) = list match { 14 | case x :: xs => true 15 | case _ => false 16 | } 17 | } 18 | """) 19 | 20 | global.ask { () => apply(rule)(tree).size should be(1) } 21 | } 22 | 23 | it should "be matched with Nil" in { 24 | val tree = fromString(""" 25 | class Toto { 26 | def toto(list : Seq[Int]) = list match { 27 | case Nil => true 28 | case _ => false 29 | } 30 | } 31 | """) 32 | 33 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 34 | } 35 | 36 | it should "not be matched by :: (even when Nil is around)" in { 37 | val tree = fromString(""" 38 | class Toto { 39 | def toto(list : Seq[Int]) = list match { 40 | case x :: xs => true 41 | case Nil => false 42 | } 43 | } 44 | """) 45 | 46 | global.ask { () => apply(rule)(tree).size should be(1) } 47 | } 48 | 49 | it should "work fine on other matchers" in { 50 | val tree = fromString(""" 51 | class Toto { 52 | def toto(list : Seq[Int]) = list match { 53 | case Seq(a, b) => true 54 | case _ => false 55 | } 56 | } 57 | """) 58 | 59 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 60 | } 61 | 62 | "Lists" should "accept :: and Nil as matchers" in { 63 | val tree = fromString(""" 64 | class Toto { 65 | def toto(list : List[Int]) = list match { 66 | case x :: xs => true 67 | case Nil => false 68 | } 69 | } 70 | """) 71 | 72 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 73 | } 74 | 75 | } 76 | -------------------------------------------------------------------------------- /rules/core/src/main/scala/com/lightbend/abide/core/PublicMutable.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide._ 4 | import scala.tools.abide.traversal._ 5 | import scala.tools.abide.directives._ 6 | import scala.reflect.internal._ 7 | 8 | object PublicMutable extends ContextGenerator { 9 | def getContext(universe: SymbolTable) = new Context(universe) with MutabilityChecker 10 | } 11 | 12 | class PublicMutable(val context: Context with MutabilityChecker) extends WarningRule { 13 | import context._ 14 | import universe._ 15 | 16 | val name = "public-mutable-fields" 17 | 18 | abstract class Warning(val tree: Tree) extends RuleWarning { 19 | val pos = tree.pos 20 | val name = tree.asInstanceOf[ValDef].name.toString.trim 21 | } 22 | 23 | case class ValWarning(vd: Tree, witness: MutableWitness) extends Warning(vd) { 24 | val message = s"${name} is a visible mutable `val`, since ${witness.withPath(Seq(name))}" 25 | } 26 | 27 | case class VarWarning(vd: Tree) extends Warning(vd) { 28 | val message = s"${name} is a public `var`, which is probably not a good idea" 29 | } 30 | 31 | val step = optimize { 32 | case valDef @ q"$mods val $name : $tpt = $value" if !mods.isSynthetic && tpt.tpe != null => 33 | if (valDef.symbol.hasGetter) { // private[this] fields have no getters, yet are obviously private 34 | val getter: Symbol = valDef.symbol.getter 35 | val owner: Symbol = valDef.symbol.owner 36 | if (getter.isPublic && (owner.isClass || owner.isModule)) publicMutable(tpt.tpe) match { 37 | case witness: MutableWitness => nok(ValWarning(valDef, witness)) 38 | case _ => 39 | } 40 | } 41 | 42 | case varDef @ q"$mods var $name : $tpt = $value" if !mods.isSynthetic => 43 | if (varDef.symbol.hasGetter) { // private[this] fields have no getters, yet are obviously private 44 | val getter: Symbol = varDef.symbol.getter 45 | val owner: Symbol = varDef.symbol.owner 46 | if (getter.isPublic && (owner.isClass || owner.isModule)) nok(VarWarning(varDef)) 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /rules/core/src/test/scala/com/lightbend/abide/core/InferAnyTest.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide.traversal._ 4 | import com.lightbend.abide.core._ 5 | 6 | class InferAnyTest extends TraversalTest { 7 | 8 | val rule = new InferAny(context) 9 | 10 | "Inferences of Any" should "not be valid if not annotated" in { 11 | val tree = fromString(""" 12 | class Test { 13 | List(1, 2, 3) contains "a" 14 | 1L to 10L contains 3 15 | } 16 | """) 17 | 18 | global.ask { () => apply(rule)(tree).size should be(2) } 19 | } 20 | 21 | it should "not be valid in methods if not annotated" in { 22 | val tree = fromString(""" 23 | class Test { 24 | def get(x: => Option[Int]) = x getOrElse Some(5) 25 | } 26 | """) 27 | 28 | global.ask { () => apply(rule)(tree).size should be(1) } 29 | } 30 | 31 | it should "be valid if applied method is explicitly applied to Any" in { 32 | val tree = fromString(""" 33 | class Test { 34 | List(1, 2, 3) contains[Any] "a" 35 | 1L to 10L contains[Any] 3 36 | } 37 | """) 38 | 39 | global.ask { () => apply(rule)(tree) shouldBe empty } 40 | } 41 | 42 | it should "be valid if the argument is explicitly ascribed Any" in { 43 | val tree = fromString(""" 44 | class Test { 45 | def get(x: => Option[Int]) = x getOrElse (Some(5): Any) 46 | } 47 | """) 48 | 49 | global.ask { () => apply(rule)(tree) shouldBe empty } 50 | } 51 | 52 | it should "not be valid only once for case classes" in { 53 | val tree = fromString(""" 54 | case class Test(x: Boolean = 1L to 10L contains 3) 55 | """) 56 | 57 | global.ask { () => apply(rule)(tree).size should be(1) } 58 | } 59 | 60 | it should "not be valid if Any is inferred as part of a tuple" in { 61 | val tree = fromString(""" 62 | class Test { 63 | ((1L to 10L) zip (11L to 20L)).contains((3, 15L)) 64 | ((1L to 10L) zip (11L to 20L)).contains(((3: Any), 15L)) 65 | } 66 | """) 67 | 68 | global.ask { () => apply(rule)(tree).size should be(1) } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /abide/src/main/scala/scala/tools/abide/Analyzer.scala: -------------------------------------------------------------------------------- 1 | package scala.tools.abide 2 | 3 | import scala.tools.nsc._ 4 | 5 | /** 6 | * Supertrait for analyzer generator objects. 7 | * 8 | * Each [[Rule]] must be assigned an [[AnalyzerGenerator]] that will instantiate the [[Analyzer]] necessary 9 | * for rule application. One [[Analyzer]] will typically apply multiple rules. 10 | * 11 | * Sometimes, a new [[Analyzer]] type can actually apply the rules from other pre-existing 12 | * [[Analyzer]] types in a more optimal or general way. To enable such extension points, the 13 | * [[AnalyzerGenerator.subsumes]] field describes which generators can be skipped once this 14 | * one has been created. 15 | * 16 | * @see [[traversal.FusingTraversalAnalyzerGenerator]] for a concrete example 17 | * @see [[Analyzer]] 18 | */ 19 | trait AnalyzerGenerator { 20 | 21 | /** 22 | * Buils a new [[Analyzer]] instance based on a compiler (scala.reflect.internal.SymbolTable), and 23 | * a list of rules. The [[Analyzer thus generated will then apply these rules to provided trees. 24 | */ 25 | def getAnalyzer(global: Global, rules: List[Rule]): Analyzer 26 | 27 | /** 28 | * Subsumption mechanism that enables optimized or generalized analyzers to replace simpler ones. 29 | * In order to subsume (ie. replace) a given analyzer, simply add it's generator to the subsumption set. 30 | */ 31 | def subsumes: Set[AnalyzerGenerator] 32 | } 33 | 34 | /** 35 | * Supertrait for [[Rule]] application classes. 36 | * 37 | * In many cases, rules can be grouped together in a logical way to optimize tree traversal, or keep the 38 | * traversal logic outside of the rules. This logic should be contained inside the [[Analyzer]] class 39 | * that will apply it's contained rules to provided trees. 40 | * 41 | * @see [[traversal.FusingTraversalAnalyzer]] for a concrete example 42 | * @see [[AnalyzerGenerator]] 43 | */ 44 | trait Analyzer { 45 | protected val global: Global 46 | import global._ 47 | 48 | /** Applies the rules contained in this [[Analyzer]] to the provided tree and return a list of new warnings. */ 49 | def apply(tree: Tree): List[Warning] 50 | } 51 | -------------------------------------------------------------------------------- /rules/core/src/test/scala/com/lightbend/abide/core/InaccessibleTest.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide.traversal._ 4 | import com.lightbend.abide.core._ 5 | 6 | class InaccessibleTest extends scala.tools.abide.traversal.TraversalTest { 7 | 8 | val rule = new Inaccessible(context) 9 | 10 | "Methods with private types" should "not be valid if those types are in their arguments" in { 11 | val tree = fromString(""" 12 | class Test { 13 | private class T 14 | def method(x: T) = 2 15 | } 16 | """) 17 | 18 | global.ask { () => apply(rule)(tree).size should be(1) } 19 | } 20 | 21 | it should "not be valid if those types are in their type bounds" in { 22 | val tree = fromString(""" 23 | package test { 24 | private[test] trait PrivateTrait 25 | 26 | trait Trait { 27 | def method[T <: PrivateTrait](x: T): T = x 28 | } 29 | } 30 | """) 31 | 32 | global.ask { () => apply(rule)(tree).size should be(1) } 33 | } 34 | 35 | it should "not be valid if those types are in their arguments' type parameters" in { 36 | val tree = fromString(""" 37 | package test { 38 | private[test] trait PrivateTrait 39 | 40 | trait Trait { 41 | def method(x: Map[Int, Set[PrivateTrait]]) = 5 42 | } 43 | } 44 | """) 45 | 46 | global.ask { () => apply(rule)(tree).size should be(1) } 47 | } 48 | 49 | it should "be valid if it is final" in { 50 | val tree = fromString(""" 51 | class Test { 52 | private trait Trait 53 | final def method1(x: Trait) = 2 54 | final def method2[T <: Trait](x: T) = x 55 | final def method3(x: Map[Int, Set[Trait]]) = 5 56 | } 57 | """) 58 | 59 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 60 | } 61 | 62 | "Methods without private types" should "be valid" in { 63 | val tree = fromString(""" 64 | class Test { 65 | trait T 66 | def method(x: T) = 2 67 | } 68 | """) 69 | 70 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /abide/src/main/scala/scala/tools/abide/traversal/PathRule.scala: -------------------------------------------------------------------------------- 1 | package scala.tools.abide.traversal 2 | 3 | import scala.reflect.internal.traversal._ 4 | 5 | /** 6 | * PathRule 7 | * 8 | * TraversalRule subtrait that provides helper methods to manage hierarchical path-dependent traversal. 9 | * The [[enter]] method will add elements to the path state, and [[State.matches]] verifies whether a 10 | * certain path is indeed contained in the path state. 11 | * 12 | * As in [[WarningRule]] and [[ScopingRule]], warnings are determined given local context and are simply 13 | * collected with the [[nok]] method. 14 | */ 15 | trait PathRule extends TraversalRule with ScopingTraversal { 16 | import context.universe._ 17 | 18 | /** Path type that will be accumulated in the path stack */ 19 | type Element 20 | 21 | def emptyState = State(Nil, Nil) 22 | case class State(path: List[Element], warnings: List[Warning]) extends RuleState { 23 | def nok(warning: Warning): State = State(path, warning :: warnings) 24 | def enter(element: Element): State = State(element :: path, warnings) 25 | def leave: State = State(path.tail, warnings) 26 | 27 | /** Provides access to the last element that was registered in the path */ 28 | def last: Option[Element] = path.headOption 29 | 30 | /** 31 | * Checks whether the accumulated path contains the provided sequence. 32 | * 33 | * The element sequence is checked in order, but other elements can exist between the elements 34 | * we're interested in in the actual path. 35 | */ 36 | def matches(seq: Element*): Boolean = seq.reverse.foldLeft[Option[List[Element]]](Some(path)) { 37 | (remaining, elem) => 38 | remaining.flatMap { elements => 39 | val dropped = elements.dropWhile(_ != elem) 40 | if (dropped.isEmpty) None else Some(dropped.tail) 41 | } 42 | }.isDefined 43 | } 44 | 45 | /** Register element as latest path element traversed (pushes onto path stack) */ 46 | def enter(element: Element): Unit = { transform(_ enter element, _.leave) } 47 | 48 | /** Report a warning */ 49 | def nok(warning: Warning): Unit = { transform(_ nok warning) } 50 | } 51 | -------------------------------------------------------------------------------- /rules/core/src/test/scala/com/lightbend/abide/core/PolyImplicitOverloadTest.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide.traversal._ 4 | import com.lightbend.abide.core._ 5 | 6 | class PolyImplicitOverloadTest extends TraversalTest { 7 | 8 | val rule = new PolyImplicitOverload(context) 9 | 10 | "Implicit methods" should "be valid when monomorphic and not overloaded" in { 11 | val tree = fromString(""" 12 | object Test { 13 | implicit def meth(x: List[Int]): Map[Int, Int] = Map() 14 | } 15 | """) 16 | 17 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 18 | } 19 | 20 | it should "be valid when polymorphic and not overloaded" in { 21 | val tree = fromString(""" 22 | object Test { 23 | implicit def meth[T](x: List[T]): Map[T, T] = Map() 24 | } 25 | """) 26 | 27 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 28 | } 29 | 30 | it should "be valid when monomorphic and overloaded" in { 31 | val tree = fromString(""" 32 | object Test { 33 | implicit def meth(x: List[Int]): Map[Int, Int] = Map() 34 | implicit def meth(x: Set[Int]): Map[Int, Int] = Map() 35 | } 36 | """) 37 | 38 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 39 | } 40 | 41 | it should "not be valid when polymorphic and overloaded" in { 42 | val tree = fromString(""" 43 | object Test { 44 | implicit def meth[T](x: List[T]): Map[T, T] = Map() 45 | implicit def meth[T](x: Set[T]): Map[T, T] = Map() 46 | } 47 | """) 48 | 49 | global.ask { () => apply(rule)(tree).size should be(2) } 50 | } 51 | 52 | "Implicit classes" should "be valid" in { 53 | val tree = fromString(""" 54 | package object foo { 55 | implicit class Bar[T](val x: T) extends AnyVal { 56 | def bippy = 1 57 | } 58 | } 59 | 60 | package foo { 61 | object Baz { 62 | def main(args: Array[String]): Unit = { 63 | "abc".bippy 64 | } 65 | } 66 | } 67 | """) 68 | 69 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /abide/src/main/scala/scala/tools/abide/traversal/NaiveTraversalAnalyzer.scala: -------------------------------------------------------------------------------- 1 | package scala.tools.abide.traversal 2 | 3 | import scala.tools.abide._ 4 | 5 | import scala.tools.nsc._ 6 | import scala.reflect.internal._ 7 | import scala.reflect.internal.traversal._ 8 | import scala.reflect.internal.util.NoPosition 9 | 10 | /** 11 | * NaiveTraversalAnalyzerGenerator 12 | * 13 | * AnalyzerGenerator that performs single-pass traversal for each given [[TraversalRule]] instance. 14 | * 15 | * @see [[NaiveTraversalAnalyzer]] 16 | */ 17 | object NaiveTraversalAnalyzerGenerator extends AnalyzerGenerator { 18 | def getAnalyzer(global: Global, rules: List[Rule]): NaiveTraversalAnalyzer = { 19 | val traversalRules = rules.flatMap(_ match { 20 | case t: TraversalRule => 21 | Some(t) 22 | case rule => 23 | global.reporter.warning(NoPosition, "Skipping unexpected type for TraversalAnalyzer : " + rule.getClass) 24 | None 25 | }) 26 | 27 | new NaiveTraversalAnalyzer(global, traversalRules) 28 | } 29 | 30 | val subsumes = Set.empty[AnalyzerGenerator] 31 | } 32 | 33 | /** 34 | * NaiveTraversalAnalyzer 35 | * 36 | * Analyzer that applies a list of [[TraversalRule]] instances to a given universe.Tree. Unlike the 37 | * [[FusingTraversalAnalyzer]], this analyzer naively performs traversal for each rule separately without fusing 38 | * the rules together. 39 | * 40 | * This analyzer is actually never be used in practice since it is subsumed by the 41 | * [[FusingTraversalAnalyzer]] packaged alongside, but it serves to display how easily new analyzers can be 42 | * plugged in to the Abide framework (ie. simply transitively subsume the previous analyzer) 43 | */ 44 | class NaiveTraversalAnalyzer(val global: Global, rules: List[TraversalRule]) extends Analyzer { 45 | import global._ 46 | 47 | def apply(tree: Tree): List[Warning] = rules.flatMap { rule => 48 | rule.asInstanceOf[Traversal { val universe: NaiveTraversalAnalyzer.this.global.type }].traverse(tree) 49 | try { 50 | rule.result.warnings 51 | } 52 | catch { 53 | case t: rule.TraversalError => 54 | global.warning(t.pos, t.message) 55 | global.debugStack(t.cause) 56 | Nil 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /rules/core/src/test/scala/com/lightbend/abide/core/RenamedDefaultParameterTest.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide.traversal._ 4 | import com.lightbend.abide.core._ 5 | 6 | class RenamedDefaultParameterTest extends TraversalTest { 7 | 8 | val rule = new RenamedDefaultParameter(context) 9 | 10 | "Methods without defaults" should "be valid when not renamed" in { 11 | val tree = fromString(""" 12 | class Toto { 13 | def test(a : Int) = a + 1 14 | } 15 | class Titi extends Toto { 16 | override def test(a : Int) = a + 2 17 | } 18 | """) 19 | 20 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 21 | } 22 | 23 | it should "be valid when renamed" in { 24 | val tree = fromString(""" 25 | class Toto { 26 | def test(a : Int) = a + 1 27 | } 28 | class Titi extends Toto { 29 | override def test(a : Int) = a + 2 30 | } 31 | """) 32 | 33 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 34 | } 35 | 36 | "Methods with defaults" should "be valid when not renamed" in { 37 | val tree = fromString(""" 38 | class Toto { 39 | def test(x : Int = 1, y : Int = 2) = x + y + 1 40 | } 41 | class Titi extends Toto { 42 | override def test(x : Int = 2, y : Int = 1) = x + y + 2 43 | } 44 | """) 45 | 46 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 47 | } 48 | 49 | it should "be valid when renamed to non-existing parameter" in { 50 | val tree = fromString(""" 51 | class Toto { 52 | def test(x : Int = 1, y : Int = 2) = x + y + 1 53 | } 54 | class Titi extends Toto { 55 | override def test(x : Int = 2, z : Int = 1) = x + z + 2 56 | } 57 | """) 58 | 59 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 60 | } 61 | 62 | it should "be invalid when renamed" in { 63 | val tree = fromString(""" 64 | class Toto { 65 | def test(x : Int = 1, y : Int = 2) = x + y + 1 66 | } 67 | class Titi extends Toto { 68 | override def test(y : Int = 2, x : Int = 1) = x + y + 2 69 | } 70 | """) 71 | 72 | global.ask { () => apply(rule)(tree).size should be(2) } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /abide/src/main/scala/scala/tools/abide/traversal/TraversalRule.scala: -------------------------------------------------------------------------------- 1 | package scala.tools.abide.traversal 2 | 3 | import scala.tools.abide._ 4 | import scala.reflect.internal.traversal._ 5 | 6 | /** 7 | * TraversalRule 8 | * 9 | * Base-class for rules based on single traversal of source tree. Traversal is powered by 10 | * classes in the scala.reflect.internal.traversal package and rules can therefore be fused 11 | * to increase traversal speed. 12 | * 13 | * Traversal is implemented by defining the `step` value of a rule, which is a partial 14 | * function from universe.Tree to Unit. Each step should update the internal state of the 15 | * traversal by using the `transform` method from the `Traversal` class. Typically, one will 16 | * provide a few helper methods in intermediate [[TraversalRule]] subclasses to make rule 17 | * definitions clearer (see [[ExistentialRule]] for example). State will only be consumed once 18 | * the traversal has terminated, so although partial states can allways be accessed, these aren't 19 | * necessarily meaningful (eg. in [[ExistentialRule]], issues can be invalidated after they were 20 | * discovered and warnings can therefore only be collected after a full traversal). 21 | * 22 | * Since we are extending the OptimizingTraversal trait, we can use the `optimize` macro on our 23 | * step function to enable traversal fusing. A step value will typically look something like : 24 | * 25 | * val step = optimize { 26 | * case vd : ValDef => // transform state somehow 27 | * case dd : DefDef => // transform state some-other-how 28 | * } 29 | * 30 | * If scoping information is needed for the rule, the ScopingTraversal trait can be mixed in 31 | * to the rule to provide the necessary helpers. Fusing and application will be seamlessly 32 | * handled by the encapsulating framework. Note that the [[NaiveTraversalAnalyzerGenerator]] 33 | * shipped in [[TraversalRule]] by default will be replaced by a [[FusingTraversalAnalyzerGenerator]] 34 | * by the framework since it subsumes the naive version (see [[scala.tools.abide.compiler.AbidePlugin]] for more info). 35 | */ 36 | trait TraversalRule extends OptimizingTraversal with Rule { 37 | val universe: context.universe.type = context.universe 38 | 39 | val analyzer = NaiveTraversalAnalyzerGenerator 40 | } 41 | -------------------------------------------------------------------------------- /rules/core/src/test/scala/com/lightbend/abide/core/PrivateShadowTest.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide.traversal._ 4 | import com.lightbend.abide.core._ 5 | 6 | class PrivateShadowTest extends TraversalTest { 7 | 8 | val rule = new PrivateShadow(context) 9 | 10 | "Field selection" should "not be valid if it accesses a private field which shadows a mutable field in the parent class" in { 11 | val tree = fromString(""" 12 | // In A, x and y are -1. 13 | class A(var x: Int) { 14 | val y: Int = -1 15 | } 16 | 17 | // In B, x and y are 99 and private[this], implicitly so 18 | // in the case of x. 19 | class B(x: Int) extends A(-1) { 20 | private[this] def y: Int = 99 21 | 22 | // Three distinct results. 23 | def f = List( 24 | /* (99,99) */ (this.x, this.y), // warn 25 | /* (-1,99) */ ((this: B).x, (this: B).y), 26 | /* (-1,-1) */ ((this: A).x, (this: A).y) 27 | ) 28 | 29 | // The 99s tell us we are reading the private[this] 30 | // data of a different instance. 31 | def g(b: B) = List( 32 | /* (-1,99) */ (b.x, b.y), 33 | /* (-1,99) */ ((b: B).x, (b: B).y), 34 | /* (-1,-1) */ ((b: A).x, (b: A).y) 35 | ) 36 | } 37 | """) 38 | 39 | global.ask { () => apply(rule)(tree).size should be(1) } 40 | } 41 | 42 | it should "not be valid if it accesses a constructor field which shadows a mutable field in the parent class" in { 43 | val tree = fromString(""" 44 | class Test { 45 | class Base(var x: Int) { def increment() = { x = x + 1 } } 46 | class Derived(x: Int) extends Base(x) { override def toString = x.toString } // warn 47 | 48 | val derived = new Derived(1) 49 | } 50 | """) 51 | 52 | global.ask { () => apply(rule)(tree).size should be(1) } 53 | } 54 | 55 | it should "be valid if it accesses a constructor field which shadows a non-mutable field in the parent class" in { 56 | val tree = fromString(""" 57 | class Test { 58 | class Base(val x: Int) 59 | class Derived(x: Int) extends Base(x) { override def toString = x.toString } // no warn 60 | } 61 | """) 62 | 63 | global.ask { () => apply(rule)(tree) shouldBe empty } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /abide/src/main/scala/scala/tools/abide/Rule.scala: -------------------------------------------------------------------------------- 1 | package scala.tools.abide 2 | 3 | import scala.reflect.internal._ 4 | 5 | /** 6 | * The bottom [[ContextGenerator]] that will be used if no other rule requires any extra context. 7 | * 8 | * @see [[ContextGenerator]] 9 | */ 10 | object Rule extends ContextGenerator { 11 | def getContext(universe: SymbolTable): Context = new Context(universe) 12 | } 13 | 14 | /** 15 | * Base class for all verification rules the framework will deal with 16 | */ 17 | trait Rule { 18 | /** 19 | * Rule context, which provides the rule with a compiler instance and more if necessary. 20 | * 21 | * @see [[Context]] 22 | */ 23 | val context: Context 24 | 25 | /** 26 | * Pointer to the [[AnalyzerGenerator]] object necessary to apply this rule. 27 | */ 28 | val analyzer: AnalyzerGenerator 29 | 30 | /** 31 | * Base trait for the state that each rule will work on during application. 32 | * 33 | * At any given time, a rule state must be able to produce a list of warnings. Typically, 34 | * this will be invoked after a full walk through a tree (or other method of rule application), 35 | * but to keep things clean, such assumptions should NOT be made. 36 | */ 37 | trait RuleState { 38 | def warnings: List[Warning] 39 | } 40 | 41 | /** 42 | * The state type we're dealing with in this rule. 43 | * 44 | * @see [[RuleState]] 45 | */ 46 | type State <: RuleState 47 | 48 | /** 49 | * Base trait for warnings each rule will emit. 50 | * 51 | * Using a common subclass makes it simpler to uniformely handle warnings, but we want a specific 52 | * type for each rule to tailor specific warnings for specific use-cases (eg. need info about 53 | * multiple trees to generate warning msg) 54 | * 55 | * We extend the [[scala.tools.abide.Warning]] base class that provides a (very) simple API for 56 | * warning consumers (like position and message). 57 | * @see [[scala.tools.abide.Warning]] 58 | */ 59 | trait RuleWarning extends scala.tools.abide.Warning { 60 | val rule: Rule = Rule.this 61 | } 62 | 63 | /** The warning type we're dealing with in this rule. @see [[RuleWarning]] */ 64 | type Warning <: RuleWarning 65 | 66 | /** We require a name field to manage rules (enable/disable) and pretty-print them */ 67 | val name: String 68 | } 69 | -------------------------------------------------------------------------------- /wiki/traversal/scoping-rules.md: -------------------------------------------------------------------------------- 1 | # Writing Scoping Rules 2 | 3 | Rules that need to track scoping information can take advantage of the `ScopingRule` base trait to do so. Much like for [warning rules](/wiki/traversal/warning-rules.md), scoping rules simply accumulate warnings by using a `nok` helper method. However, they also provide a scoping helper that enables simple scope accumulation: 4 | ```scala 5 | def enter(owner : Owner) : Unit 6 | ``` 7 | will add `owner` to the scoping stack and the `state` provider can be queried to check whether we are currently in a particular owner with another helper: 8 | ```scala 9 | def in(owner : Onwer) : Boolean 10 | ``` 11 | 12 | To illustrate `ScopingRule` usage, we will implement a recursive method definition checker that searches for weird definitions that point to themselves (see the full source at [StupidRecursion](/rules/core/src/main/scala/com/lightbend/abide/core/StupidRecursion.scala)). 13 | 14 | We start by defining the rule class: 15 | ```scala 16 | import scala.tools.abide._ 17 | import scala.tools.abide.traversal._ 18 | 19 | class StupidRecursion(val context : Context) extends ScopingRule { 20 | val name = "stupid-recursion" 21 | } 22 | ``` 23 | and we provide the required `Warning` type: 24 | ```scala 25 | case class Warning(tree : Tree) extends RuleWarning { 26 | val pos = tree.pos 27 | val message = s"The value $tree is recursively used " + 28 | "in it's directly defining scope" 29 | } 30 | ``` 31 | 32 | To manage scoping, we want to register entry of any method definition in the `step` partial function. To implement this, we use the `enter` helper and apply it to any member `def` declaration discovered during AST traversal: 33 | ```scala 34 | case defDef @ q"def $name : $tpt = $body" => enter(defDef.symbol) 35 | ``` 36 | 37 | Scope is automatically handled by the `ScopingRule` trait, and we can now query the internal traversal state with `state.in(sym)` to evaluate the current scoping state. 38 | 39 | Now that we have scope, we implement stupid recursion checking by simply verifying that definition access doesn't point to the current scope (`state in tree.symbol`): 40 | ```scala 41 | case id @ Ident(_) if id.symbol != null && (state in id.symbol) => 42 | nok(Warning(id)) 43 | case s @ Select(_, _) if s.symbol != null && (state in s.symbol) => 44 | nok(Warning(s)) 45 | ``` 46 | 47 | And... we're done! 48 | -------------------------------------------------------------------------------- /macros/src/test/resources/traversal/SimpleInterpreter.scala: -------------------------------------------------------------------------------- 1 | package examples 2 | 3 | object simpleInterpreter { 4 | 5 | case class M[A](value: A) { 6 | def bind[B](k: A => M[B]): M[B] = k(value) 7 | def map[B](f: A => B): M[B] = bind(x => unitM(f(x))) 8 | def flatMap[B](f: A => M[B]): M[B] = bind(f) 9 | } 10 | 11 | def unitM[A](a: A): M[A] = M(a) 12 | 13 | def showM(m: M[Value]): String = m.value.toString(); 14 | 15 | type Name = String 16 | 17 | trait Term; 18 | case class Var(x: Name) extends Term 19 | case class Con(n: Int) extends Term 20 | case class Add(l: Term, r: Term) extends Term 21 | case class Lam(x: Name, body: Term) extends Term 22 | case class App(fun: Term, arg: Term) extends Term 23 | 24 | trait Value 25 | case object Wrong extends Value { 26 | override def toString() = "wrong" 27 | } 28 | case class Num(n: Int) extends Value { 29 | override def toString() = n.toString() 30 | } 31 | case class Fun(f: Value => M[Value]) extends Value { 32 | override def toString() = "" 33 | } 34 | 35 | type Environment = List[Pair[Name, Value]] 36 | 37 | def lookup(x: Name, e: Environment): M[Value] = e match { 38 | case List() => unitM(Wrong) 39 | case Pair(y, b) :: e1 => if (x == y) unitM(b) else lookup(x, e1) 40 | } 41 | 42 | def add(a: Value, b: Value): M[Value] = Pair(a, b) match { 43 | case Pair(Num(m), Num(n)) => unitM(Num(m + n)) 44 | case _ => unitM(Wrong) 45 | } 46 | 47 | def apply(a: Value, b: Value): M[Value] = a match { 48 | case Fun(k) => k(b) 49 | case _ => unitM(Wrong) 50 | } 51 | 52 | def interp(t: Term, e: Environment): M[Value] = t match { 53 | case Var(x) => lookup(x, e) 54 | case Con(n) => unitM(Num(n)) 55 | case Add(l, r) => for (a <- interp(l, e); 56 | b <- interp(r, e); 57 | c <- add(a, b)) 58 | yield c 59 | case Lam(x, t) => unitM(Fun(a => interp(t, Pair(x, a) :: e))) 60 | case App(f, t) => for (a <- interp(f, e); 61 | b <- interp(t, e); 62 | c <- apply(a, b)) 63 | yield c 64 | } 65 | 66 | def test(t: Term): String = 67 | showM(interp(t, List())) 68 | 69 | val term0 = App(Lam("x", Add(Var("x"), Var("x"))), Add(Con(10), Con(11))) 70 | val term1 = App(Con(1), Con(2)) 71 | 72 | def main(args: Array[String]) { 73 | println(test(term0)) 74 | println(test(term1)) 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /macros/src/main/scala/scala/reflect/internal/traversal/ScopingTraversal.scala: -------------------------------------------------------------------------------- 1 | package scala.reflect.internal.traversal 2 | 3 | /** 4 | * ScopingTraversal 5 | * 6 | * Extension of the [[Traversal]] trait that provides entering _and_ leaving state transformations. 7 | * These enable users to maintain scoping information during traversals so one can define an 8 | * `is in` relation between state and trees (or symbols or whatever else one wishes to scope against). 9 | * 10 | * The scoping information can be updated by using the transform(enter : State => State, leave : State => State) method 11 | * whose second parameter will be viewd as a "leaving function". This function will be applied to the 12 | * state when the traversal leaves the current node (as opposed to the first argument function which is 13 | * applied when the traversal enters the node). 14 | * 15 | * For example, in the tree 16 | * 17 | * A 18 | * | 19 | * B 20 | * / \ 21 | * C D 22 | * 23 | * we have the following enter/leave sequence: 24 | * enter(A) -> enter(B) -> enter(C) -> leave(C) -> enter(D) -> leave(D) -> leave(B) -> leave(A) 25 | * 26 | * With such a scheme, we can easily deal with scoping by pushing and popping scoping information with 27 | * the two functions given to the transform method. 28 | * 29 | * @see [[ScopingTraversalFusion]] 30 | */ 31 | trait ScopingTraversal extends Traversal { 32 | import universe._ 33 | 34 | private var leaver: Option[State => State] = None 35 | 36 | /** 37 | * The [[ScopingTraversal]] transformer can also specify a leaving transformation that will be applied 38 | * to the internal state once the traversal leaves the current tree node. 39 | * 40 | * @see [[ScopingTraversalFusion]] 41 | */ 42 | protected[traversal] def transform(enter: State => State, leave: State => State): Unit = { 43 | transform(enter) 44 | leaver = Some(leaver match { 45 | case Some(l) => leave andThen l 46 | case None => leave 47 | }) 48 | } 49 | 50 | /** 51 | * Consumes leaver option, used by [[ScopingTraversalFusion]] to deal with leaver functions. 52 | * 53 | * @see [[ScopingTraversalFusion]] 54 | */ 55 | protected[traversal] def consumeLeaver(): Option[State => State] = { 56 | val l = leaver 57 | leaver = None 58 | l 59 | } 60 | 61 | protected[traversal] override def init: Unit = { 62 | super.init 63 | leaver = None 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /rules/core/src/test/scala/com/lightbend/abide/core/ByNameRightAssociativeTest.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide.traversal._ 4 | import com.lightbend.abide.core._ 5 | 6 | class ByNameRightAssociativeTest extends TraversalTest { 7 | 8 | val rule = new ByNameRightAssociative(context) 9 | 10 | "Right-associative operators" should "be valid when not using by-name parameters" in { 11 | val tree = fromString(""" 12 | class Test { 13 | def m_:(x: Int) = () 14 | } 15 | """) 16 | 17 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 18 | } 19 | 20 | it should "not be valid when using by-name parameters" in { 21 | val tree = fromString(""" 22 | class Test { 23 | def m_:(x: => Int) = () 24 | } 25 | """) 26 | 27 | global.ask { () => apply(rule)(tree).size should be(1) } 28 | } 29 | 30 | it should "not be valid for each method with by-name parameters" in { 31 | val tree = fromString(""" 32 | class Test { 33 | def m_:(x: => Int) = () 34 | def m2_:(x: => Int, y: => Char) = () 35 | private def m3_:(x: Int, y: => Char) = () 36 | private def m4_:(x: Int) = () 37 | } 38 | """) 39 | 40 | global.ask { () => apply(rule)(tree).size should be(3) } 41 | } 42 | 43 | it should "not be valid with by-name parameters in the first argument list" in { 44 | val tree = fromString(""" 45 | class Test { 46 | def op3_:(x: Any, y: => Any)(a: Any) = () 47 | } 48 | """) 49 | 50 | global.ask { () => apply(rule)(tree).size should be(1) } 51 | } 52 | 53 | it should "be valid with by-name parameters in the second argument list" in { 54 | val tree = fromString(""" 55 | class Test { 56 | def op6_:(x: Any)(a: => Any) = () 57 | } 58 | """) 59 | 60 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 61 | } 62 | 63 | "Left associative operators" should "be valid when using by-name parameters" in { 64 | val tree = fromString(""" 65 | class Test { 66 | def m(x: => Int) = () 67 | } 68 | """) 69 | 70 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 71 | } 72 | 73 | it should "be valid when not using by-name parameters" in { 74 | val tree = fromString(""" 75 | class Test { 76 | def m(x: => Int) = () 77 | def m2(x: => Int, y: => Char) = () 78 | def m3(x: Int, y: => Char) = () 79 | def m4(x: Int) = () 80 | } 81 | """) 82 | 83 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /abide/src/main/scala/scala/tools/abide/traversal/FusingTraversalAnalyzer.scala: -------------------------------------------------------------------------------- 1 | package scala.tools.abide.traversal 2 | 3 | import scala.tools.abide._ 4 | 5 | import scala.tools.nsc._ 6 | import scala.reflect.internal._ 7 | import scala.reflect.internal.traversal._ 8 | import scala.reflect.internal.util.NoPosition 9 | 10 | /** 11 | * FusingTraversalAnalyzerGenerator 12 | * 13 | * AnalyzerGenerator for fused single-pass traversals (ie. [[TraversalRule]] subtypes). 14 | * 15 | * Subsumes [[NaiveTraversalAnalyzerGenerator]] since both traversal implementations are equivalent (but 16 | * fused traversals are faster). 17 | * 18 | * @see [[FusingTraversalAnalyzer]] 19 | */ 20 | object FusingTraversalAnalyzerGenerator extends AnalyzerGenerator { 21 | def getAnalyzer(global: Global, rules: List[Rule]): FusingTraversalAnalyzer = { 22 | val traversalRules = rules.flatMap(_ match { 23 | case t: TraversalRule => 24 | Some(t) 25 | case rule => 26 | global.reporter.warning(NoPosition, "Skipping unexpected rule type for TraversalAnalyzer : " + rule.getClass) 27 | None 28 | }) 29 | 30 | new FusingTraversalAnalyzer(global, traversalRules) 31 | } 32 | 33 | val subsumes: Set[AnalyzerGenerator] = Set(NaiveTraversalAnalyzerGenerator) 34 | } 35 | 36 | /** 37 | * FusingTraversalAnalyzer 38 | * 39 | * Analyzer that applies a list of [[TraversalRule]] instances to a given universe.Tree. The traversals 40 | * specified in the TraversalRules are fused into a single-pass traversal that optimizes speed by relying 41 | * on a scala.reflect.internal.traversal.TraversalFusion (type based traversal performance enhancer). 42 | */ 43 | class FusingTraversalAnalyzer(val global: Global, rules: List[TraversalRule]) extends Analyzer { 44 | import global._ 45 | 46 | private val fused: Option[TraversalFusion { val universe: FusingTraversalAnalyzer.this.global.type }] = 47 | if (rules.isEmpty) None else Some(Fuse(global)(rules.map { rule => 48 | assert(rule.context.universe == global, "Missmatch between analyzer and rule universe") 49 | rule.asInstanceOf[Traversal { val universe: FusingTraversalAnalyzer.this.global.type }] 50 | }: _*)) 51 | 52 | def apply(tree: Tree): List[Warning] = fused match { 53 | case Some(traverser) => 54 | traverser.traverse(tree) 55 | rules.flatMap { rule => 56 | try { 57 | rule.result.warnings 58 | } 59 | catch { 60 | case t: rule.TraversalError => 61 | global.warning(t.pos, t.message) 62 | global.debugStack(t.cause) 63 | Nil 64 | } 65 | }.toList 66 | case None => Nil 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /abide/src/test/scala/scala/tools/abide/traversal/WarningRuleTest.scala: -------------------------------------------------------------------------------- 1 | package scala.tools.abide.traversal 2 | 3 | import scala.tools.abide._ 4 | import scala.tools.abide.traversal._ 5 | 6 | class WarningRuleTest extends AbideTest { 7 | 8 | val context = new Context(global) 9 | import global._ 10 | 11 | object warningRule extends { 12 | val context: WarningRuleTest.this.context.type = WarningRuleTest.this.context 13 | } with WarningRule { 14 | import context.universe._ 15 | 16 | val name = "warning-rule" 17 | 18 | case class Warning(vd: ValDef) extends RuleWarning { 19 | val pos = vd.pos 20 | val message = "haha failed!" 21 | } 22 | 23 | val step = optimize { 24 | case defDef: DefDef if !defDef.symbol.isSynthetic && !defDef.symbol.owner.isSynthetic => 25 | defDef.symbol.overrides.foreach { overriden => 26 | (defDef.vparamss.flatten zip overriden.asMethod.paramLists.flatten).foreach { 27 | case (vd, o) => 28 | if (vd.symbol.name != o.name) { 29 | nok(Warning(vd)) 30 | } 31 | } 32 | } 33 | } 34 | } 35 | 36 | def warningTraverse(tree: Tree): Set[ValDef] = { 37 | var valDefs: Set[ValDef] = Set.empty 38 | 39 | def rec(tree: Tree): Unit = tree match { 40 | case defDef: DefDef if !defDef.symbol.isSynthetic && !defDef.symbol.owner.isSynthetic => 41 | defDef.symbol.overrides.foreach { overriden => 42 | (defDef.vparamss.flatten zip overriden.asMethod.paramLists.flatten).foreach { 43 | case (vd, o) => 44 | if (vd.symbol.name != o.name) { 45 | valDefs += vd 46 | } 47 | } 48 | } 49 | defDef.children.foreach(rec(_)) 50 | case _ => tree.children.foreach(rec(_)) 51 | } 52 | 53 | rec(tree) 54 | valDefs 55 | } 56 | 57 | "Warning rule validity" should "be ensured in AddressBook.scala" in { 58 | val tree = fromFile("traversal/AddressBook.scala") 59 | global.ask { () => 60 | warningRule.traverse(tree.asInstanceOf[warningRule.universe.Tree]) 61 | val ruleResult = warningRule.result.warnings.map(_.vd).toSet 62 | val recResult = warningTraverse(tree) 63 | 64 | ruleResult should be(recResult) 65 | } 66 | } 67 | 68 | it should "be ensured in SimpleInterpreter.scala" in { 69 | val tree = fromFile("traversal/SimpleInterpreter.scala") 70 | global.ask { () => 71 | warningRule.traverse(tree.asInstanceOf[warningRule.universe.Tree]) 72 | val ruleResult = warningRule.result.warnings.map(_.vd).toSet 73 | val recResult = warningTraverse(tree) 74 | 75 | ruleResult should be(recResult) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /rules/core/src/main/scala/com/lightbend/abide/core/UnusedMember.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide._ 4 | import scala.tools.abide.traversal._ 5 | 6 | class UnusedMember(val context: Context) extends ExistentialRule { 7 | import context.universe._ 8 | 9 | val name = "unused-member" 10 | type Key = Symbol 11 | 12 | case class Warning(tree: Tree) extends RuleWarning { 13 | val pos = tree.pos 14 | val message = s"${tree.symbol} is not used." 15 | } 16 | 17 | private def shouldConsider(sym: Symbol): Boolean = { 18 | val owner = sym.owner 19 | val isValueParameter = owner.isMethod && sym.isValueParameter 20 | 21 | val isMainArgs = isValueParameter && owner.name == TermName("main") && (owner.asMethod.paramLists match { 22 | case List(List(param)) => param.typeSignature == typeOf[Array[String]] 23 | case _ => false 24 | }) 25 | 26 | val ignore = ((isValueParameter && owner.isDeferred) // abstract method, params are never used 27 | || isMainArgs // args : Array[String] don't need to be used since they're a language requirement for the main 28 | || owner.isConstructor // right after typer constructors are empty, so their params are not used 29 | || sym.isSynthetic 30 | || sym.isConstant // must ignore constant types since these are folded during type checking 31 | || sym.name.containsChar('$') // synthetic names that are not marked as SYNTHETIC: `(_, _) => ???` 32 | || isValueParameter && 33 | (owner.hasFlag(Flag.OVERRIDE) || owner.overrides.nonEmpty)) // overriding a method needs to keep all (unused) parameters 34 | 35 | def getter(sym: Symbol): Symbol = sym.getter 36 | val privateVal = if (getter(sym) != NoSymbol) getter(sym).isPrivate else sym.isPrivate 37 | 38 | !ignore && 39 | (sym.isLocalToBlock || 40 | (sym.isMethod && sym.isPrivate) || 41 | ((sym.isVal || sym.isVar) && privateVal)) 42 | } 43 | 44 | val step = optimize { 45 | case vd: ValDef if shouldConsider(vd.symbol) => 46 | nok(vd.symbol, Warning(vd)) 47 | 48 | case dd: DefDef if shouldConsider(dd.symbol) => 49 | nok(dd.symbol, Warning(dd)) 50 | 51 | case tree @ q"$pre.$name" => 52 | ok(tree.symbol) 53 | 54 | case b: Bind => 55 | nok(b.symbol, Warning(b)) 56 | 57 | case tree @ Ident(_) => 58 | val affectedSymbols = 59 | if (tree.symbol.owner.isConstructor) { 60 | val paramAccessors = tree.symbol.enclClass.constrParamAccessors 61 | paramAccessors.find(tree.symbol.name == _.name).toSeq :+ tree.symbol 62 | } 63 | else 64 | Seq(tree.symbol) 65 | affectedSymbols.foreach(ok(_)) 66 | } 67 | } 68 | 69 | -------------------------------------------------------------------------------- /rules/core/src/test/scala/com/lightbend/abide/core/DelayedInitSelectTest.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide.traversal._ 4 | import com.lightbend.abide.core._ 5 | 6 | class DelayedInitSelectTest extends TraversalTest { 7 | 8 | val rule = new DelayedInitSelect(context) 9 | 10 | "Selection from DelayedInit" should "be valid in the extended object" in { 11 | val tree = fromString(""" 12 | object O extends App { 13 | val vall = "" 14 | 15 | println(vall) 16 | } 17 | """) 18 | 19 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 20 | } 21 | 22 | it should "be valid in constructions called in the extended object" in { 23 | val tree = fromString(""" 24 | object O extends App { 25 | val vall = "" 26 | 27 | new { println(vall) } 28 | } 29 | """) 30 | 31 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 32 | } 33 | 34 | it should "not be valid from a different object" in { 35 | val tree = fromString(""" 36 | object O extends App { 37 | val vall = "" 38 | } 39 | 40 | object Client { 41 | println(O.vall) 42 | } 43 | """) 44 | 45 | global.ask { () => apply(rule)(tree).size should be(1) } 46 | } 47 | 48 | it should "not be valid from a different object when imported" in { 49 | val tree = fromString(""" 50 | object O extends App { 51 | val vall = "" 52 | } 53 | 54 | object Client { 55 | import O.vall 56 | println(vall) 57 | } 58 | """) 59 | 60 | global.ask { () => apply(rule)(tree).size should be(1) } 61 | } 62 | 63 | it should "be valid when selecting a lazy value" in { 64 | val tree = fromString(""" 65 | object O extends App { 66 | lazy val vall = "" 67 | } 68 | 69 | object Client { 70 | println(O.vall) 71 | } 72 | """) 73 | 74 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 75 | } 76 | 77 | it should "be valid when selecting a method" in { 78 | val tree = fromString(""" 79 | object O extends App { 80 | def method = "" 81 | } 82 | 83 | object Client { 84 | println(O.method) 85 | } 86 | """) 87 | 88 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 89 | } 90 | 91 | it should "be valid when selecting from a superclass other than the DelayedInit" in { 92 | val tree = fromString(""" 93 | trait T { 94 | val traitVal = "" 95 | } 96 | 97 | object O extends App with T { 98 | def method = "" 99 | } 100 | 101 | object Client { 102 | println(O.traitVal) 103 | } 104 | """) 105 | 106 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /macros/src/test/scala/scala/reflect/internal/traversal/TreeProvider.scala: -------------------------------------------------------------------------------- 1 | package scala.reflect.internal.traversal 2 | 3 | import scala.tools.nsc.io._ 4 | import scala.tools.nsc.interactive._ 5 | import scala.reflect.internal.util._ 6 | 7 | /** 8 | * trait TreeProvider 9 | * 10 | * Trait that provides access to trees given strings, file names, etc and also provides a few 11 | * convenience methods on these trees. Mostly useful for writing tests. 12 | */ 13 | trait TreeProvider extends CompilerProvider { 14 | 15 | private val randomFileName = { 16 | val r = new java.util.Random 17 | () => "/tmp/abideInput" + r.nextInt + ".scala" 18 | } 19 | 20 | def fromSource(source: SourceFile): global.Tree = { 21 | val response = new global.Response[global.Tree] 22 | 23 | global.ask { () => 24 | global.askLoadedTyped(source, true, response) 25 | } 26 | 27 | response.get match { 28 | case Left(tree) => tree 29 | case Right(ex) => throw ex 30 | } 31 | } 32 | 33 | def fromFile(fileName: String): global.Tree = { 34 | val fileURL = getClass.getClassLoader.getResource(fileName) 35 | val filePath = new java.io.File(fileURL.toURI).getAbsolutePath 36 | val source = new BatchSourceFile(AbstractFile.getFile(filePath)) 37 | fromSource(source) 38 | } 39 | 40 | def fromFolder(folderName: String) = { 41 | import java.io.File 42 | import scala.collection.JavaConversions._ 43 | 44 | def getFileTree(f: File): Stream[File] = 45 | f #:: (if (f.isDirectory) f.listFiles().toStream.flatMap(getFileTree) else Stream.empty) 46 | 47 | val folder = new File(folderName) 48 | val files = getFileTree(folder).filter(file => file.isFile && file.getName.endsWith(".scala")) 49 | files.map(f => fromSource(global.getSourceFile(f.getAbsolutePath))) 50 | } 51 | 52 | def fromString(str: String): global.Tree = { 53 | val response = new Response[global.Tree] 54 | val fileName = randomFileName() 55 | val file = new BatchSourceFile(fileName, str) 56 | fromSource(file) 57 | } 58 | 59 | def classByName(tree: global.Tree, str: String): global.Symbol = { 60 | def rec(tree: global.Tree): Option[global.Symbol] = tree match { 61 | case cd @ global.ClassDef(_, name, tparams, impl) if name.toString == str => Some(cd.symbol) 62 | case md @ global.ModuleDef(_, name, impl) if name.toString == str => Some(md.symbol) 63 | case _ => tree.children.flatMap(rec(_)).headOption 64 | } 65 | 66 | rec(tree).get 67 | } 68 | 69 | def memberByName(tree: global.Tree, str: String): global.Tree = { 70 | def rec(tree: global.Tree): Option[global.Tree] = tree match { 71 | case vd @ global.ValDef(_, name, _, _) => Some(vd) 72 | case dd @ global.DefDef(_, name, _, _, _, _) => Some(dd) 73 | case _ => tree.children.flatMap(rec(_)).headOption 74 | } 75 | 76 | rec(tree).get 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /rules/extra/src/test/scala/com/typesafe/abide/extra/FixedNameOverridesTest.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.extra 2 | 3 | import scala.tools.abide.traversal._ 4 | import com.lightbend.abide.extra._ 5 | 6 | class FixedNameOverridesTest extends TraversalTest { 7 | 8 | val rule = new FixedNameOverrides(context) 9 | 10 | "Methods overrides" should "be valid when not renamed" in { 11 | val tree = fromString(""" 12 | class Toto { 13 | def test(a : Int) = a + 1 14 | } 15 | class Titi extends Toto { 16 | override def test(a : Int) = a + 2 17 | } 18 | """) 19 | 20 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 21 | } 22 | 23 | it should "not be valid when renamed" in { 24 | val tree = fromString(""" 25 | class Toto { 26 | def test(a : Int) = a + 1 27 | } 28 | class Titi extends Toto { 29 | override def test(b : Int) = b + 2 30 | } 31 | """) 32 | 33 | global.ask { () => apply(rule)(tree).size should be(1) } 34 | } 35 | 36 | it should "not be valid when renamed abstracts" in { 37 | val tree = fromString(""" 38 | trait Toto { 39 | def test(a : Int, b : String) : Int 40 | } 41 | class Titi extends Toto { 42 | def test(a : Int, c : String) = a + 1 43 | } 44 | """) 45 | 46 | global.ask { () => apply(rule)(tree).size should be(1) } 47 | } 48 | 49 | it should "not be valid for each argument" in { 50 | val tree = fromString(""" 51 | trait Toto { 52 | def test(a : Int, b : String) : Int 53 | } 54 | class Titi extends Toto { 55 | def test(e : Int, c : String) = e + 1 56 | } 57 | """) 58 | 59 | global.ask { () => apply(rule)(tree).size should be(2) } 60 | } 61 | 62 | it should "not be valid for each override" in { 63 | val tree = fromString(""" 64 | trait Toto { 65 | def test(a : Int, b : String) : Int 66 | } 67 | class Titi extends Toto { 68 | def test(a : Int, c : String) = a + 1 69 | } 70 | class Tata extends Toto { 71 | def test(e : Int, b : String) = e 72 | } 73 | """) 74 | 75 | global.ask { () => apply(rule)(tree).size should be(2) } 76 | } 77 | 78 | it should "be valid in case-class definitions" in { 79 | val tree = fromString(""" 80 | case class Person(asdbfds : Int) 81 | """) 82 | 83 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 84 | } 85 | 86 | it should "be valid in partial function desugaring" in { 87 | val tree = fromString(""" 88 | trait Toto { 89 | val a : PartialFunction[String, Unit] = { 90 | case "abc" => () 91 | case _ => 92 | } 93 | } 94 | """) 95 | 96 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /rules/core/src/main/scala/com/lightbend/abide/core/TypeParameterShadow.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide._ 4 | import scala.tools.abide.traversal._ 5 | 6 | class TypeParameterShadow(val context: Context) extends ScopingRule { 7 | import context.universe._ 8 | 9 | val name = "type-parameter-shadow" 10 | 11 | case class Warning(tp: TypeDef, sym1: Symbol, sym2: Option[Symbol]) extends RuleWarning { 12 | val pos: Position = tp.pos 13 | val message: String = 14 | if (sym2.nonEmpty) s"Type parameter ${tp.name} defined in $sym1 shadows ${sym2.get} defined in ${sym2.get.owner}. You may want to rename your type parameter, or possibly remove it." 15 | else s"Type parameter ${tp.name} defined in $sym1 shadows predefined type ${tp.name}" 16 | 17 | } 18 | 19 | // State is a pair of List[Symbol] (the declared type parameters) and Boolean 20 | // (true if the declarer is a class or method or type member) 21 | type Owner = Symbol 22 | 23 | // Any of the types in scala.Predef could be shadowed 24 | val predefTypes = List("List", "Double", "Float", "Long", "Int", "Char", "Short", "Byte", "Unit", "Boolean") ++ scala.reflect.runtime.universe.typeOf[scala.Predef.type].decls.filter(_.isType).map(_.name.toString) 25 | 26 | def getDeclaration(tp: TypeDef, scope: List[Owner]) = scope.find { sym => 27 | sym.name == tp.name && sym.isType 28 | } 29 | 30 | def enclClassOrMethodOrTypeMemberScope: List[Owner] = state.scope.dropWhile { sym => 31 | !sym.isClass && !sym.isMethod && !(sym.isType && !sym.isParameter) 32 | } 33 | 34 | def warnTypeParameterShadow(tparams: List[TypeDef], sym: Symbol) = { 35 | if (!sym.isSynthetic) { 36 | val tt = tparams.filter(_.name != typeNames.WILDCARD).foreach { tp => 37 | if (predefTypes.contains(tp.name.toString)) nok(Warning(tp, sym, None)) 38 | // We don't care about type params shadowing other type params in the same declaration 39 | else { 40 | getDeclaration(tp, enclClassOrMethodOrTypeMemberScope) foreach { prevTp => 41 | if (prevTp != tp.symbol) { 42 | nok(Warning(tp, sym, Some(prevTp))) 43 | } 44 | } 45 | } 46 | 47 | } 48 | } 49 | } 50 | 51 | def enterAll(possTypes: List[Symbol]) = possTypes.map(enter(_)) 52 | 53 | val step = optimize { 54 | case t @ Template(parents, self, body) => 55 | enterAll(t.tpe.members.toList) 56 | case p @ PackageDef(pid, stats) => 57 | enterAll(pid.tpe.members.toList) 58 | 59 | case cd @ ClassDef(_, _, tparams, _) => 60 | enter(cd.symbol) 61 | warnTypeParameterShadow(tparams, cd.symbol) 62 | enterAll(tparams.map(_.symbol)) 63 | case dd @ DefDef(_, _, tparams, _, _, _) => 64 | enter(dd.symbol) 65 | warnTypeParameterShadow(tparams, dd.symbol) 66 | enterAll(tparams.map(_.symbol)) 67 | case td @ TypeDef(_, _, tparams, _) => 68 | enter(td.symbol) 69 | warnTypeParameterShadow(tparams, td.symbol) 70 | enterAll(tparams.map(_.symbol)) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /rules/core/src/test/scala/com/lightbend/abide/core/PublicMutableTest.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide.traversal._ 4 | import com.lightbend.abide.core._ 5 | 6 | class PublicMutableTest extends TraversalTest { 7 | import scala.tools.abide.traversal._ 8 | 9 | val rule = new PublicMutable(context) 10 | 11 | "Immutability" should "be guaranteed for public vals" in { 12 | val tree = fromString(""" 13 | class Toto { 14 | val toto : List[Int] = Nil 15 | } 16 | """) 17 | 18 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 19 | } 20 | 21 | it should "not matter in public defs" in { 22 | val tree = fromString(""" 23 | class Mut { private var a : Int = 0 } 24 | class Toto { 25 | def toto : Mut = new Mut 26 | } 27 | """) 28 | 29 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 30 | } 31 | 32 | it should "not matter in private vals" in { 33 | val tree = fromString(""" 34 | class Mut { private var a : Int = 0 } 35 | class Toto { 36 | private val toto : Mut = new Mut 37 | } 38 | """) 39 | 40 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 41 | } 42 | 43 | it should "not matter in private[this] members" in { 44 | val tree = fromString(""" 45 | class Toto { 46 | private[this] val sb = new StringBuilder 47 | } 48 | """) 49 | 50 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 51 | } 52 | 53 | it should "not matter for self-member" in { 54 | val tree = fromString(""" 55 | trait Toto extends Mutable { self => 56 | val i = 1 57 | } 58 | """) 59 | 60 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 61 | } 62 | 63 | "Mutability" should "be warned about in public vals" in { 64 | val tree = fromString(""" 65 | class Mut { var a : Int = 0 } 66 | class Toto { 67 | val mut : Mut = new Mut 68 | } 69 | """) 70 | 71 | global.ask { () => 72 | val syms = apply(rule)(tree).map(_.tree.symbol.toString) 73 | syms.sorted should be(List("value mut", "variable a")) 74 | } 75 | } 76 | 77 | it should "be warned about in public vars" in { 78 | val tree = fromString(""" 79 | class Toto { 80 | var a = 0 81 | } 82 | """) 83 | 84 | global.ask { () => 85 | val syms = apply(rule)(tree).map(_.tree.symbol.toString) 86 | syms.sorted should be(List("variable a")) 87 | } 88 | } 89 | 90 | it should "be warned about in public mutable vars" in { 91 | val tree = fromString(""" 92 | class Mut { private var a : Int = 0 } 93 | class Toto { 94 | var toto : Mut = new Mut 95 | } 96 | """) 97 | 98 | global.ask { () => 99 | val syms = apply(rule)(tree).map(_.tree.symbol.toString) 100 | syms.sorted should be(List("variable toto")) 101 | } 102 | } 103 | 104 | } 105 | -------------------------------------------------------------------------------- /rules/core/src/test/scala/com/lightbend/abide/core/NullaryOverrideTest.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide.traversal._ 4 | import com.lightbend.abide.core._ 5 | 6 | class NullaryOverrideTest extends TraversalTest { 7 | 8 | val rule = new NullaryOverride(context) 9 | 10 | "Non-nullary methods" should "be valid when not overriding anything" in { 11 | val tree = fromString(""" 12 | class Test { 13 | def nonNullary() = 123 14 | } 15 | """) 16 | 17 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 18 | } 19 | 20 | it should "be valid when overriding other non-nullary methods" in { 21 | val tree = fromString(""" 22 | class Parent { 23 | def nonNullary() = 123 24 | } 25 | class Child extends Parent { 26 | override def nonNullary() = 456 27 | } 28 | """) 29 | 30 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 31 | } 32 | 33 | it should "not be valid when overriding nullary methods" in { 34 | val tree = fromString(""" 35 | class Parent { 36 | def nullary = 123 37 | } 38 | class Child extends Parent { 39 | override def nullary() = 456 40 | } 41 | """) 42 | 43 | global.ask { () => apply(rule)(tree).size should be(1) } 44 | } 45 | 46 | it should "be invalid only once when overriding an overridden nullary method" in { 47 | val tree = fromString(""" 48 | class A { 49 | def nullary = 123 50 | } 51 | class B extends A { 52 | override def nullary = 456 53 | } 54 | class C extends B { 55 | override def nullary() = 789 56 | } 57 | """) 58 | 59 | global.ask { () => apply(rule)(tree).size should be(1) } 60 | } 61 | 62 | it should "be invalid only once when overriding multiple nullary methods" in { 63 | val tree = fromString(""" 64 | trait A { 65 | def nullary = 123 66 | } 67 | trait B { 68 | def nullary = 456 69 | } 70 | trait C { 71 | def nullary() = 789 72 | } 73 | class D extends A with B with C { 74 | override def nullary() = 0 75 | } 76 | """) 77 | 78 | global.ask { () => apply(rule)(tree).size should be(1) } 79 | } 80 | 81 | "Nullary methods" should "be valid when overriding non-nullary methods" in { 82 | val tree = fromString(""" 83 | class Parent { 84 | def nonNullary() = 123 85 | } 86 | class Child extends Parent { 87 | override def nonNullary = 456 88 | } 89 | """) 90 | 91 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 92 | } 93 | 94 | it should "be valid when overriding other nullary methods" in { 95 | val tree = fromString(""" 96 | class Parent { 97 | def nonNullary = 123 98 | } 99 | class Child extends Parent { 100 | override def nonNullary = 456 101 | } 102 | """) 103 | 104 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /macros/src/main/scala/scala/reflect/internal/traversal/TraversalFusion.scala: -------------------------------------------------------------------------------- 1 | package scala.reflect.internal.traversal 2 | 3 | /** 4 | * TraversalFusion 5 | * 6 | * Takes care of actual traversal fusing. 7 | */ 8 | trait TraversalFusion { 9 | protected val universe: scala.reflect.api.Universe 10 | import universe._ 11 | 12 | private[traversal]type TraversalType = Traversal { val universe: TraversalFusion.this.universe.type } 13 | private[traversal]type FusionType = TraversalFusion { val universe: TraversalFusion.this.universe.type } 14 | 15 | /** List of all traversals we're fusing */ 16 | val traversals: Seq[TraversalType] 17 | 18 | /** Adds a new [[Traversal]] to the list of fused traversals */ 19 | def fuse(that: TraversalType): FusionType = Fuse(universe)(traversals :+ that: _*) 20 | 21 | /** Adds all the traversals contained in a [[TraversalFusion]] to the list of fused traversals */ 22 | def fuse(that: FusionType): FusionType = Fuse(universe)(traversals ++ that.traversals: _*) 23 | 24 | /** 25 | * We compute these maps lazily to avoid unnecessary computations during 26 | * fuse foldings where we don't actually want to use the partial fusings 27 | * and should therefore not lose time optimizing them 28 | */ 29 | private lazy val (classToTraversals, allClassTraversals) = { 30 | var classToTraversals: Map[Class[_], Set[TraversalType]] = Map.empty 31 | var allClassTraversals: Set[TraversalType] = Set.empty 32 | 33 | for (traversal <- traversals) { 34 | val classes = traversal match { 35 | case optimizer: OptimizingTraversal => optimizer.step match { 36 | case optimizer.ClassExtraction(classes, _) => classes 37 | case _ => None 38 | } 39 | 40 | case _ => None 41 | } 42 | 43 | classes match { 44 | case Some(matchClasses) => matchClasses.foreach { clazz => 45 | classToTraversals += (clazz -> (classToTraversals.getOrElse(clazz, Set.empty) + traversal)) 46 | } 47 | case None => 48 | allClassTraversals += traversal 49 | } 50 | } 51 | 52 | (classToTraversals, allClassTraversals) 53 | } 54 | 55 | /** Make sure all internal structures necessary for traversal have been computed */ 56 | def force: this.type = { 57 | classToTraversals 58 | allClassTraversals 59 | this 60 | } 61 | 62 | /** Access relevant traversals given a concrete tree instance (lookup by class and fallback for un-optimized traversals) */ 63 | protected def getTraversals(tree: Tree): List[TraversalType] = { 64 | (classToTraversals.getOrElse(tree.getClass, Nil) ++ allClassTraversals).toList 65 | } 66 | 67 | private def foreach(tree: Tree)(f: Tree => Unit): Unit = { 68 | def rec(tree: Tree): Unit = { 69 | f(tree) 70 | tree.children.foreach(rec(_)) 71 | } 72 | 73 | rec(tree) 74 | } 75 | 76 | /** Applies the fused traversals to a tree (in a foreach manner) */ 77 | def traverse(tree: Tree): Unit = { 78 | traversals.foreach(_.init) 79 | foreach(tree)(tree => getTraversals(tree).foreach { 80 | traversal => traversal.apply(tree) 81 | }) 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /wiki/traversal/existential-rules.md: -------------------------------------------------------------------------------- 1 | # Writing Existential Rules 2 | 3 | We will illustrate here the `ExistentialRule` base-trait that extends `TraversalRule`. Existential rules generate warnings by traversing the whole tree and validating / invalidating shared _keys_ that are extracted from tree nodes. An invalid key will remain so until it is validated, and validated keys cannot be invalidated. This structure is necessary since we can't guarantee key visiting order, so we must make sure there is a precedence relation. Once a full traversal has been performed, warnings are generated based on the keys that remain invalid. 4 | 5 | The `ExistentialRule` trait therefore defines a `State` type that encodes the behaviour we just described. Furthermore, it provides two helper methods: 6 | ```scala 7 | def ok(key : Key) : Unit 8 | def nok(key : Key, warning : Warning) : Unit 9 | ``` 10 | that will validate (`ok`), respectively invalidate (`nok`), these keys. The `Key` type remains an abstract member of the `ExistentialRule` type since different types of keys can be used in different concrete rules. Note that `nok` also takes a `Warning` argument that specifies the concrete warning that will be output by the rule if the associated key remains invalid after a full AST traversal. 11 | 12 | To showcase an `ExistentialRule` usage example, we will present a rule that checks that every private `var` member of a class is assigned somewhere in the class body (or it should be a `val`). Since we can't assume anything about declaration / assignment ordering, we use `ExistentialRule` as a base-trait and validate assignments while invalidating variable declarations. You can see the full source at [ValInsteadOfVar](/rules/core/src/main/scala/com/lightbend/abide/core/ValInsteadOfVar.scala). 13 | 14 | Let us start by defining the rule class: 15 | ```scala 16 | import scala.tools.abide._ 17 | import scala.tools.abide.traversal._ 18 | 19 | class MemberValInsteadOfVar(val context : Context) extends ExistentialRule { 20 | val name : String = "member-val-instead-of-var" 21 | } 22 | ``` 23 | 24 | The next step is to define the warning type. This is done by providing a `Warning` case class that provides a meaningful warning message: 25 | ```scala 26 | import context.universe._ 27 | 28 | case class Warning(tree : Tree) extends RuleWarning { 29 | val pos = tree.pos 30 | val message = s"The member `var` $tree was never assigned " + 31 | "locally and should therefore be declared as a `val`" 32 | } 33 | ``` 34 | 35 | Finally, once the groundwork has been laid, we need to perform the actual verification. As mentioned previously, we use the `ok` and `nok` helper methods to validate / invalidate the `var` symbols when these are encountered in the tree walk. Since we are only interested by declarations and assignments, we only need to match those trees in the `step` partial function. 36 | ```scala 37 | val step = optimize { 38 | case varDef @ q"$mods var $name : $tpt = $value" => 39 | val setter : Symbol = varDef.symbol.setter 40 | // only invalidate private variables 41 | if (setter.isPrivate) nok(varDef.symbol, Warning(varDef)) 42 | 43 | // assignments are handled internally through setters, 44 | // so that's what we search for! 45 | case set @ q"$setter(..$args)" if setter.symbol.isSetter => 46 | // the variable is valid once assigned 47 | ok(setter.symbol.accessed) 48 | } 49 | ``` 50 | 51 | And... we're done! 52 | -------------------------------------------------------------------------------- /wiki/traversal/warning-rules.md: -------------------------------------------------------------------------------- 1 | # Writing Warning Rules 2 | 3 | Let us illustrate the `WarningRule` base-trait by writing a rule that makes sure overrides don't change argument names (see [FixedNameOverrides](/rules/extra/src/main/scala/com/lightbend/abide/extra/FixedNameOverrides.scala) for the full source). Since the typing phase will provide us with symbols for all definitions that a particular member overrides, we only need local information to verify whether a definition is valid or not, and we can verify the rule in a single pass through the program AST. Therefore, the `WarningRule` trait is exactly what we need! 4 | 5 | The `WarningRule` trait defines a `State` type that simply accumulates warnings. These warnings are added to the state by invoking the `nok(warning : Warning) : Unit` helper provided by the `WarningRule trait. This is pretty much the simplest way possible of generating warnings during a traversal. 6 | 7 | First off, let's start by writing a new class that extends the `WarningRule` trait. 8 | ```scala 9 | import scala.tools.abide._ // brings Context into scope 10 | import scala.tools.abide.traversal._ // brings the WarningRule trait into scope 11 | 12 | class FixedNameOverrides(val context : Context) extends WarningRule { 13 | val name = "fixed-name-overrides" 14 | } 15 | ``` 16 | 17 | Once we have the base class, we can define the shape of the warnings this rule will report by defining the type member `Warning` of the `Rule` trait. 18 | ```scala 19 | import context.universe._ // brings scala syntax trees into scope 20 | 21 | case class Warning(vd : ValDef, sn : String, sym : MethodSymbol) extends RuleWarning { 22 | val pos = vd.pos 23 | val message = s"Renaming parameter ${vd.name} of method ${vd.symbol.owner.name}" + 24 | s"from $sn in super-type ${sym.owner.name} can lead to confusion" 25 | } 26 | ``` 27 | To make sure we have a meaningful warning message, we define the `Warning` type with all the necessary parameters. We see here why having users provide their own `Warning` type helps making reporting clear and precise. 28 | 29 | The only remaining member that needs a definition is the `step` value which will actually describe warning accumulation. Since we're dealing with method overrides checking, we only need to consider `DefDef` trees during traversal, and we use the `optimize` macro to benefit from a fusion speedup: 30 | ```scala 31 | val step = optimize { 32 | case defDef : DefDef => 33 | check(defDef) 34 | } 35 | ``` 36 | where the `check` function can be implemented as 37 | ```scala 38 | def check(defDef : DefDef) { 39 | if (!defDef.symbol.isSynthetic && !defDef.symbol.owner.isSynthetic) { 40 | // iterate over all definitions that defDef overrides 41 | defDef.symbol.overrides.foreach { overriden => 42 | 43 | // iterate over all parameter pairs (defDef.param, overriden.param) which are in the same position 44 | (defDef.vparamss.flatten zip overriden.asMethod.paramLists.flatten).foreach { case (vd, o) => 45 | 46 | // if the name changed, then we emit a warning 47 | if (vd.symbol.name != o.name) { 48 | // we use the `nok` helper method we described previously 49 | nok(Warning(vd, o.name.toString, overriden.asMethod)) 50 | } 51 | } 52 | } 53 | } 54 | } 55 | ``` 56 | 57 | And... we're done! Now don't forget to add the rule to the `resources/abide-plugin.xml` file for the sbt **abide** plugin to automatically detect this rule when verifying your scala projects. 58 | 59 | -------------------------------------------------------------------------------- /abide/src/test/scala/scala/tools/abide/directives/MutabilityTest.scala: -------------------------------------------------------------------------------- 1 | package scala.tools.abide.directives 2 | 3 | import scala.tools.abide._ 4 | import scala.tools.abide.directives._ 5 | import scala.tools.nsc._ 6 | 7 | class MutabilityTest extends AbideTest { 8 | 9 | object mutable extends { 10 | val universe: MutabilityTest.this.global.type = MutabilityTest.this.global 11 | } with MutabilityChecker 12 | 13 | import global._ 14 | 15 | def mutableClass(tree: Tree, str: String): Boolean = global.ask { () => 16 | val tpe = classByName(tree, str).toType 17 | mutable.publicMutable(tpe).isMutable 18 | } 19 | 20 | "Type immutability" should "be ensured in Lists" in { 21 | val tree = fromString(""" 22 | sealed abstract class List[T] 23 | case class Cons[T](head: T, tail: List[T]) extends List[T] 24 | case object Nil extends List[Nothing] 25 | """) 26 | 27 | mutableClass(tree, "List") should be(false) 28 | mutableClass(tree, "Cons") should be(false) 29 | mutableClass(tree, "Nil") should be(false) 30 | } 31 | 32 | it should "be ensured in Trees" in { 33 | val tree = fromString(""" 34 | sealed abstract class Tree[T] 35 | case class Node[T](t1: Tree[T], t2: Tree[T]) extends Tree[T] 36 | case class Leaf[T](value: T) extends Tree[T] 37 | """) 38 | 39 | mutableClass(tree, "Tree") should be(false) 40 | mutableClass(tree, "Node") should be(false) 41 | mutableClass(tree, "Leaf") should be(false) 42 | } 43 | 44 | it should "be ensured in types with immutable fields" in { 45 | val tree = fromString(""" 46 | class ImmutableFields(a: Int, b: Boolean) { 47 | val map : Map[Int,Int] = Map(a -> 0) 48 | } 49 | class ImmutableFields2(list: List[Map[Int,List[_]]]) 50 | """) 51 | 52 | mutableClass(tree, "ImmutableFields") should be(false) 53 | mutableClass(tree, "ImmutableFields2") should be(false) 54 | } 55 | 56 | it should "be ensured in types with cyclic references" in { 57 | val tree = fromString(""" 58 | class A(b: B) 59 | class B { 60 | val a = new A(this) 61 | } 62 | """) 63 | 64 | mutableClass(tree, "A") should be(false) 65 | mutableClass(tree, "B") should be(false) 66 | } 67 | 68 | "Type mutability" should "be ensured in types with vars" in { 69 | val tree = fromString(""" 70 | case class Test(head: Int, var tail: Int) 71 | """) 72 | 73 | mutableClass(tree, "Test") should be(true) 74 | } 75 | 76 | /* Well, maybe not... 77 | it should "be ensured in parents of mutable types" in { 78 | val tree = fromString(""" 79 | sealed abstract class Parent[T] 80 | case class Child[T](head: T, private var tail: T) extends Parent[T] 81 | """) 82 | 83 | mutableClass(tree, "Parent") should be (true) 84 | } 85 | */ 86 | 87 | /* Well, maybe not... 88 | it should "be ensured in parents of mutable objects" in { 89 | val tree = fromString(""" 90 | sealed abstract class Parent[T] 91 | case class Child[T](head: T, private val tail: T) extends Parent[T] 92 | case object Mut extends Parent[Nothing] { 93 | var a = 0 94 | } 95 | """) 96 | 97 | mutableClass(tree, "Parent") should be (true) 98 | } 99 | */ 100 | 101 | it should "be ensured in generically mutable types" in { 102 | val tree = fromString(""" 103 | class Container { 104 | val p = (1, 2, scala.collection.mutable.Map.empty[Int,Int]) 105 | } 106 | """) 107 | 108 | mutableClass(tree, "Container") 109 | } 110 | 111 | } 112 | -------------------------------------------------------------------------------- /abide/src/main/scala/scala/tools/abide/directives/MutabilityChecker.scala: -------------------------------------------------------------------------------- 1 | package scala.tools.abide.directives 2 | 3 | import scala.reflect.internal._ 4 | import scala.reflect.internal.util._ 5 | 6 | import scala.collection.mutable.{ Map => MutableMap } 7 | 8 | sealed abstract class Witness { 9 | def isMutable: Boolean 10 | } 11 | 12 | abstract class MutableWitness extends Witness { 13 | val path: Seq[String] 14 | def isMutable: Boolean = true 15 | def withPath(path: Seq[String]): MutableWitness = this match { 16 | case pvw: PublicVarWitness => pvw.copy(path = path ++ MutableWitness.this.path) 17 | case emw: ExtendsMutableWitness => emw.copy(path = path ++ MutableWitness.this.path) 18 | } 19 | } 20 | 21 | case class PublicVarWitness(path: Seq[String]) extends MutableWitness { 22 | override def toString: String = s"${path.mkString(".")} is a public `var`" 23 | } 24 | 25 | case class ExtendsMutableWitness(path: Seq[String], tpt: String) extends MutableWitness { 26 | override def toString: String = s"${path.mkString(".")} is a public `val` of type $tpt <: Mutable" 27 | } 28 | 29 | case object NoWitness extends Witness { 30 | def isMutable: Boolean = false 31 | } 32 | 33 | trait MutabilityChecker { 34 | 35 | val universe: SymbolTable 36 | 37 | import universe._ 38 | import universe.definitions._ 39 | import scala.collection.immutable.Set 40 | 41 | private val publicMutableCache: MutableMap[Type, Witness] = MutableMap.empty 42 | 43 | def publicMutable(tpt: Type): Witness = { 44 | def rec(tpts: List[(Type, Seq[String])], seen: Set[Type]): Witness = { 45 | var nextTpes: List[(Type, Seq[String])] = List.empty 46 | var nextSeen: Set[Type] = seen 47 | 48 | val witnessOpt = tpts.view.flatMap { 49 | case (tpe, path) => 50 | val res: Option[MutableWitness] = publicMutableCache.get(tpe) match { 51 | case Some(witness: MutableWitness) => Some(witness) 52 | case Some(NoWitness) => None 53 | case None => 54 | val wtpeOpt = if (tpe.typeSymbol.isClass && tpe.typeSymbol.asClass.baseClasses.exists(_ == symbolOf[scala.Mutable])) { 55 | Some(ExtendsMutableWitness(Seq.empty, tpe.toString)) 56 | } 57 | else tpe.members.view.flatMap { member => 58 | if (member.isPublic && member.isSetter) { // if member is a var, we are clearly mutable 59 | Some(PublicVarWitness(Seq(member.accessed.name.toString))) 60 | } 61 | else if (!member.isSynthetic && member.isPublic && member.isVal) { 62 | // we use the seen set here to manage type cycles (these are immutable since we know the cycle 63 | // doesn't point to a var, as we've established this above) 64 | val termType = member.typeSignature 65 | if (!nextSeen(termType)) { 66 | nextSeen += termType 67 | nextTpes :+= (termType, path :+ member.name.toString) 68 | } 69 | 70 | None 71 | } 72 | else { 73 | None 74 | } 75 | }.headOption 76 | 77 | if (wtpeOpt.isDefined) publicMutableCache(tpe) = wtpeOpt.get 78 | wtpeOpt 79 | } 80 | 81 | res.map(witness => witness.withPath(path)) 82 | }.headOption 83 | 84 | witnessOpt match { 85 | case Some(witness) => witness 86 | case None if nextTpes.isEmpty => NoWitness 87 | case _ => rec(nextTpes, nextSeen) 88 | } 89 | } 90 | 91 | rec(List(tpt -> Seq.empty[String]), Set(tpt)) 92 | } 93 | 94 | } 95 | -------------------------------------------------------------------------------- /abide/src/test/scala/scala/tools/abide/traversal/ErroneousRuleTest.scala: -------------------------------------------------------------------------------- 1 | package scala.tools.abide.traversal 2 | 3 | import scala.tools.abide._ 4 | import scala.tools.abide.traversal._ 5 | 6 | class ErroneousRuleTest extends AbideTest { 7 | 8 | override def silent = true 9 | 10 | val context = new Context(global) 11 | import global._ 12 | 13 | object warningRule extends { 14 | val context: ErroneousRuleTest.this.context.type = ErroneousRuleTest.this.context 15 | } with WarningRule { 16 | import context.universe._ 17 | 18 | val name = "warning-rule" 19 | 20 | case class Warning(vd: ValDef) extends RuleWarning { 21 | val pos = vd.pos 22 | val message = "haha failed!" 23 | } 24 | 25 | val step = optimize { 26 | case defDef: DefDef if !defDef.symbol.isSynthetic && !defDef.symbol.owner.isSynthetic => 27 | defDef.symbol.overrides.foreach { overriden => 28 | (defDef.vparamss.flatten zip overriden.asMethod.paramLists.flatten).foreach { 29 | case (vd, o) => 30 | if (vd.symbol.name != o.name) { 31 | nok(Warning(vd)) 32 | } 33 | } 34 | } 35 | } 36 | } 37 | 38 | def warningTraverse(tree: Tree): Set[ValDef] = { 39 | var valDefs: Set[ValDef] = Set.empty 40 | 41 | def rec(tree: Tree): Unit = tree match { 42 | case defDef: DefDef if !defDef.symbol.isSynthetic && !defDef.symbol.owner.isSynthetic => 43 | defDef.symbol.overrides.foreach { overriden => 44 | (defDef.vparamss.flatten zip overriden.asMethod.paramLists.flatten).foreach { 45 | case (vd, o) => 46 | if (vd.symbol.name != o.name) { 47 | valDefs += vd 48 | } 49 | } 50 | } 51 | defDef.children.foreach(rec(_)) 52 | case _ => tree.children.foreach(rec(_)) 53 | } 54 | 55 | rec(tree) 56 | valDefs 57 | } 58 | 59 | object failingRule extends { 60 | val context: ErroneousRuleTest.this.context.type = ErroneousRuleTest.this.context 61 | } with WarningRule { 62 | import context.universe._ 63 | 64 | val name = "failing-rule" 65 | 66 | case object Warning extends RuleWarning { 67 | val pos = NoPosition 68 | val message = "faiiiillllleeeedd" 69 | } 70 | 71 | val step = optimize { 72 | case valDef: ValDef => 73 | valDef.symbol.asClass 74 | } 75 | } 76 | 77 | val traverser = FusingTraversalAnalyzerGenerator.getAnalyzer(global, List(warningRule, failingRule)) 78 | 79 | "Rule failure" should "be isolated in AddressBook.scala" in { 80 | val tree = fromFile("traversal/AddressBook.scala") 81 | global.ask { () => 82 | traverser(tree.asInstanceOf[traverser.global.Tree]) 83 | val ruleResult = warningRule.result.warnings.map(_.vd).toSet 84 | val recResult = warningTraverse(tree) 85 | 86 | ruleResult should be(recResult) 87 | 88 | val failed = try { 89 | failingRule.result 90 | false 91 | } 92 | catch { 93 | case t: failingRule.TraversalError => true 94 | } 95 | 96 | failed should be(true) 97 | } 98 | } 99 | 100 | it should "be isolated in SimpleInterpreter.scala" in { 101 | val tree = fromFile("traversal/SimpleInterpreter.scala") 102 | global.ask { () => 103 | traverser(tree.asInstanceOf[traverser.global.Tree]) 104 | val ruleResult = warningRule.result.warnings.map(_.vd).toSet 105 | val recResult = warningTraverse(tree) 106 | 107 | ruleResult should be(recResult) 108 | 109 | val failed = try { 110 | failingRule.result 111 | false 112 | } 113 | catch { 114 | case t: failingRule.TraversalError => true 115 | } 116 | 117 | failed should be(true) 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /rules/core/src/test/scala/com/lightbend/abide/core/EmptyOrNonEmptyUsingSizeTest.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide.traversal.TraversalTest 4 | 5 | class EmptyOrNonEmptyUsingSizeTest extends TraversalTest { 6 | 7 | val rule = new EmptyOrNonEmptyUsingSize(context) 8 | 9 | "Using size == 0 on List" should "give a warning" in { 10 | val tree = fromString(""" 11 | class Test { 12 | List().size == 0 13 | } """) 14 | 15 | global.ask { () => apply(rule)(tree).nonEmpty should be(true) } 16 | } 17 | 18 | "Using size != 0 on List" should "give a warning" in { 19 | val tree = fromString(""" 20 | class Test { 21 | List().size != 0 22 | } """) 23 | 24 | global.ask { () => apply(rule)(tree).nonEmpty should be(true) } 25 | } 26 | 27 | "Using length == 0 on List" should "give a warning" in { 28 | val tree = fromString(""" 29 | class Test { 30 | List().length == 0 31 | } """) 32 | 33 | global.ask { () => apply(rule)(tree).nonEmpty should be(true) } 34 | } 35 | 36 | "Using length != 0 on List" should "give a warning" in { 37 | val tree = fromString(""" 38 | class Test { 39 | List().length != 0 40 | } """) 41 | 42 | global.ask { () => apply(rule)(tree).nonEmpty should be(true) } 43 | } 44 | 45 | "Using size == 0 on Seq" should "give a warning" in { 46 | val tree = fromString(""" 47 | class Test { 48 | Seq().size == 0 49 | } """) 50 | 51 | global.ask { () => apply(rule)(tree).nonEmpty should be(true) } 52 | } 53 | 54 | "Using size != 0 on Seq" should "give a warning" in { 55 | val tree = fromString(""" 56 | class Test { 57 | Seq().size != 0 58 | } """) 59 | 60 | global.ask { () => apply(rule)(tree).nonEmpty should be(true) } 61 | } 62 | 63 | "Using length == 0 on Seq" should "give a warning" in { 64 | val tree = fromString(""" 65 | class Test { 66 | Seq().length == 0 67 | } """) 68 | 69 | global.ask { () => apply(rule)(tree).nonEmpty should be(true) } 70 | } 71 | 72 | "Using length != 0 on Seq" should "give a warning" in { 73 | val tree = fromString(""" 74 | class Test { 75 | Seq().length != 0 76 | } """) 77 | 78 | global.ask { () => apply(rule)(tree).nonEmpty should be(true) } 79 | } 80 | 81 | "Using size == 0 on Set" should "give a warning" in { 82 | val tree = fromString(""" 83 | class Test { 84 | Set().size == 0 85 | } """) 86 | 87 | global.ask { () => apply(rule)(tree).nonEmpty should be(true) } 88 | } 89 | 90 | "Using size != 0 on Set" should "give a warning" in { 91 | val tree = fromString(""" 92 | class Test { 93 | Set().size != 0 94 | } """) 95 | 96 | global.ask { () => apply(rule)(tree).nonEmpty should be(true) } 97 | } 98 | 99 | "Using size == 0 on Map" should "give a warning" in { 100 | val tree = fromString(""" 101 | class Test { 102 | Map().size == 0 103 | } """) 104 | 105 | global.ask { () => apply(rule)(tree).nonEmpty should be(true) } 106 | } 107 | 108 | "Using size != 0 on Map" should "give a warning" in { 109 | val tree = fromString(""" 110 | class Test { 111 | Map().size != 0 112 | } """) 113 | 114 | global.ask { () => apply(rule)(tree).nonEmpty should be(true) } 115 | } 116 | 117 | "Using size or length == 0 on objects not subclasses to Traversable" should "not give a warning" in { 118 | val tree = fromString(""" 119 | class Test { 120 | class Something { 121 | def size = 0 122 | } 123 | 124 | val s = new Something 125 | s.size == 0 126 | }""") 127 | 128 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 129 | } 130 | 131 | } 132 | -------------------------------------------------------------------------------- /rules/core/src/test/scala/com/lightbend/abide/core/TypeParameterShadowTest.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide.traversal._ 4 | import com.lightbend.abide.core._ 5 | 6 | class TypeParameterShadowTest extends TraversalTest { 7 | 8 | val rule = new TypeParameterShadow(context) 9 | 10 | "Method type parameter" should "not be valid if it shadows another type" in { 11 | val tree = fromString(""" 12 | trait D 13 | trait Test { 14 | def foobar[D](in: D) = in.toString // warn 15 | } 16 | """) 17 | 18 | global.ask { () => apply(rule)(tree).size should be(1) } 19 | } 20 | 21 | it should "not be valid if it shadows another type when nested" in { 22 | val tree = fromString(""" 23 | trait Test { 24 | def foobar[N[M[List[_]]]] = 1 // warn 25 | } 26 | """) 27 | 28 | global.ask { () => apply(rule)(tree).size should be(1) } 29 | } 30 | 31 | it should "be valid if it has the same name as another parameter in the same list" in { 32 | val tree = fromString(""" 33 | trait Test { 34 | def foobar[A, N[M[L[A]]]] = 1 // no warn 35 | } 36 | """) 37 | 38 | global.ask { () => apply(rule)(tree) shouldBe empty } 39 | } 40 | 41 | "Type member type parameter" should "not be valid if it shadows another type" in { 42 | val tree = fromString(""" 43 | trait D 44 | trait Test { 45 | type MySeq[D] = Seq[D] // warn 46 | } 47 | """) 48 | 49 | global.ask { () => apply(rule)(tree).size should be(1) } 50 | } 51 | 52 | it should "not be valid if it shadows another type when nested" in { 53 | val tree = fromString(""" 54 | trait Test { 55 | type E[M[List[_]]] = Int // warn 56 | } 57 | """) 58 | 59 | global.ask { () => apply(rule)(tree).size should be(1) } 60 | } 61 | 62 | it should "be valid if it has the same name as another parameter in the same list" in { 63 | val tree = fromString(""" 64 | trait Test { 65 | type G[A, M[L[A]]] = Int // no warn 66 | } 67 | """) 68 | 69 | global.ask { () => apply(rule)(tree) shouldBe empty } 70 | } 71 | 72 | "Class type parameter" should "not be valid if it shadows another type" in { 73 | val tree = fromString(""" 74 | trait Test { 75 | trait T 76 | class Foo[T](t: T) { // warn 77 | def bar[T](w: T) = w.toString // warn 78 | } 79 | } 80 | """) 81 | 82 | global.ask { () => apply(rule)(tree).size should be(2) } 83 | } 84 | 85 | it should "not be valid if it shadows another type when nested" in { 86 | val tree = fromString(""" 87 | class C[M[List[_]]] // warn 88 | """) 89 | 90 | global.ask { () => apply(rule)(tree).size should be(1) } 91 | } 92 | 93 | it should "be valid if it has the same name as another parameter in the same list" in { 94 | val tree = fromString(""" 95 | class F[A, M[L[A]]] // no warn 96 | """) 97 | 98 | global.ask { () => apply(rule)(tree) shouldBe empty } 99 | } 100 | 101 | it should "not be valid if the parameter's name is List" in { 102 | val tree = fromString(""" 103 | class G[List] // warn 104 | """) 105 | 106 | global.ask { () => apply(rule)(tree).size should be(1) } 107 | } 108 | 109 | it should "not be valid if the parameter's name is Byte" in { 110 | val tree = fromString(""" 111 | class F[Byte] // warn 112 | """) 113 | 114 | global.ask { () => apply(rule)(tree).size should be(1) } 115 | } 116 | 117 | it should "not be valid if it shadows a type alias" in { 118 | val tree = fromString(""" 119 | object TypeAliasTest { 120 | type T = Int 121 | class F[T] // warn 122 | } 123 | """) 124 | 125 | global.ask { () => apply(rule)(tree).size should be(1) } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /wiki/traversal/traversal-rules.md: -------------------------------------------------------------------------------- 1 | # Writing Traversal Rules 2 | 3 | Many lint rules can be verified by a single pass through the program source code. To make writing such rules easy, **abide** provides the `TraversalRule` trait as extension point. 4 | 5 | Traversal rules collect warnings by maintaining internal state and visiting each tree node once. Default traversal has no guarantees on traversal ordering and rules must therefore be completely agnostic. However, one can request hierarchical traversal ordering (parents visited before children) by mixing the `ScopingTraversal` trait into the rule definition. 6 | 7 | ## Existing helpers 8 | 9 | In addition to being verifiable with a single pass, rules will typically share some common behaviour even within the `TraversalRule` context, and three sub-trait extensions are defined by the **abide** framework: 10 | 11 | 1. [warning rules](/wiki/traversal/warning-rules.md) provide helper methods by extending the `WarningRule` trait to collect local warnings that don't depend on any context / external information. Such rules will typically rely on typer information to determine validity of a tree. 12 | 13 | 2. [existential rules](/wiki/traversal/existential-rules.md) rely on more than local context and will need to collect information from different parts of a tree to verify a property. A recurring pattern uses validation / invalidation of certain _keys_ depending on local context and is provided by the `ExistentialRule` trait. In an `ExistentialRule`, once a key has been marked as valid, it will remain so for ever, whereas invalid keys can be validated (and will then remain valid). After a full pass through an AST, keys that remain invalid will generate warnings. 14 | 15 | 3. [scoping rules](/wiki/traversal/scoping-rules.md) rely on scoping information (by mixing `ScopingTraversal` in) to verify program properties, a situation that will generally arise when verifying properties dealing with recursion or inner / outer relations. The `ScopingRule` interfaces provides helpers to manage and access scope during AST traversal to verify such properties. 16 | 17 | ## Adding new helper traits 18 | 19 | Many traversal rules can be implemented by using the previous helper base-traits, but some may require a slightly different state representation, or different helpers. Here are a few considerations to keep in mind when writing new traversal base-traits. 20 | 21 | ### Initial state 22 | 23 | The internal state maintained by traversal rules is defined by two abstract members of `TraversalRule`, namely 24 | ```scala 25 | type State <: RuleState 26 | def emptyState : State 27 | ``` 28 | where `State` defines the concrete internal state type and `emptyState` provides initial state before a traversal begins. Actual initialization is automatically and internally managed by the traversal analyzer. 29 | 30 | The `State` type must simply define the `def warnings : List[Warning]` method that converts the current state to a list of framework warnings that will be consumed by the framework once traversal is finished. 31 | 32 | ### State transformation 33 | 34 | To define the actual state transformation applied at each tree node, one must define the 35 | ```scala 36 | val step : PartialFunction[Tree, Unit] 37 | ``` 38 | member of the `TraversalRule` subtype. This partial function relies on 39 | ```scala 40 | def transform(f : State => State) : Unit 41 | ``` 42 | that updates the internal state of the traversal rule. If `ScopingTraversal` has been mixed in, we also gain access to 43 | ```scala 44 | def transform(enter : State => State, leave : State => State) : Unit 45 | ``` 46 | where the `leave` function is applied to the state when the traversal leaves a node. In practice, the `transform` functions are typically not called in user-defined rules but are abstracted away by helper methods such as `WarningRule.nok` or `ScopingRule.enter`. 47 | 48 | To enable traversal rule fusing (which can drastically increase performance), one can use the `optimize` macro surrounding the `step` partial function: 49 | ```scala 50 | val step = optimize { 51 | case vd : ValDef => 52 | case dd : DefDef => 53 | } 54 | ``` 55 | -------------------------------------------------------------------------------- /wiki/rules.md: -------------------------------------------------------------------------------- 1 | # Abide Rules 2 | 3 | **Abide** combines flexibility and power by managing simple rules with complex analyzers. Typically, these analyzers will be provided by the framework maintainers but can be contributed by users as well. However, the main focus of user contributions should be on **abide** rules. 4 | 5 | ## Contributing a rule 6 | 7 | Once a new rule has been defined, it must be added to a rule project. These are defined in the `project/Build.scala` sbt build configuration as sub-projects that point to some file path in `scala-abide/rules`. All such rule packages will be separately exported as jars that can be selectively used for project verification. Typically, unless the new rule must logically belong to a new package, it suffices to place the rule inside one of the existing rule `src` folders and it will automatically be built alongside the pre-existing rules. 8 | 9 | To specify all rules provided by a rule package, each rule project must contain an xml file `resources/abide-plugin.xml` that specifies which rule classes are provided by that particular package. Build tool integration relies on these xml files to reflectively instantiate rules, so extending these files is mandatory for each new rule. The file structure is as follows: 10 | ```xml 11 | 12 | 13 | 14 | ... 15 | 16 | ``` 17 | 18 | Finally, to make sure new extensions are clear/valid and remain so, each rule must feature a short descriptive entry in the corresponding rule project wiki page that clarifies use cases and analysis outputs as well as some unit tests that will help avoid breakage. 19 | 20 | Once these steps have been performed, simply submit a pull request to this repository for your rule to be added to the community distribution of **abide**! 21 | 22 | ## Rule basics 23 | 24 | The `Rule` trait therefore only defines a few abstract members: 25 | 26 | ```scala 27 | val context : Context 28 | ``` 29 | The `context` value provides shared context between rules. The basic `Context` type simply provides a pointer to `universe : scala.reflect.api.Universe` and provides the compiler cake. The `context` value should be provided by the rule constructor. Actually, since **abide** rules are instantiated reflectively, the _only_ acceptable constructor signature for rules takes a single `val context : Context` as argument. 30 | 31 | --- 32 | 33 | ```scala 34 | val analyzer : AnalyzerGenerator 35 | ``` 36 | The `analyzer` value specifies which sort of analysis should take care of this rule. This value will typically be filled by a specialized rule base-type (like `TraversalRule`) and won't be managed by the rule writer. 37 | 38 | --- 39 | 40 | ```scala 41 | type State <: RuleState 42 | type Warning <: RuleWarning 43 | ``` 44 | The `State` type determines what kind of internal state this rule will be dealing with, and is linked to the `Warning` type through the `RuleState.warnings` method. These type members enforce a certain internal structure and type-safety on rule outputs while remaining quite general to deal with future analyzer definitions. 45 | 46 | --- 47 | 48 | ```scala 49 | val name : String 50 | ``` 51 | Finally, the `name` value provides a human-readable interface to **abide** rules for debugging and other pretty printing related activities. 52 | 53 | ## Traversal rules 54 | 55 | A [traversal rule](/wiki/traversal/traversal-rules.md) is used when verification can be performed by a single unstructured pass through the unit-local (typed) source AST. This is typically the case for context-independent rules where bad patterns can be identified without requiring any knowledge of surrounding code, but more subtle types of properties can also be verified without requiring multiple passes. 56 | 57 | Examples of such rules can be found at 58 | - [vars which are never assigned](/rules/core/src/main/scala/com/lightbend/abide/core/ValInsteadOfVar.scala) 59 | - [unused members / arguments / locals](/rules/core/src/main/scala/com/lightbend/abide/core/UnusedMember.scala) 60 | - [renaming parameters with defaults in override](/rules/core/src/main/scala/com/lightbend/abide/core/RenamedDefaultParameter.scala) 61 | - ... 62 | 63 | ## Moar rulez! 64 | 65 | Other types of rules should be coming to **abide** in the future, such as cross-unit and flow-sensitive verification, but no such work exists as of now. 66 | -------------------------------------------------------------------------------- /macros/src/main/scala/scala/reflect/internal/traversal/Traversal.scala: -------------------------------------------------------------------------------- 1 | package scala.reflect.internal.traversal 2 | 3 | /** 4 | * Traversal 5 | * 6 | * Fusable traversal base class 7 | * 8 | * Declared in seperate context from [[TraversalFusion]] to align with Abide rules 9 | * so these can be defined in their own file. This provides a nicer API for users, but makes universe 10 | * sharing harder. This difficulty is however completely hidden from the end-user and dealt with internally. 11 | * 12 | * Traversals are performed by a [[TraversalFusion]] instance which will automatically deal with initialization. 13 | * 14 | * Once the traversal has been performed (ie. foreach is done), the final state of the traversal can be accessed 15 | * through the [[result]] method. If an exception occured during traversal, a [[TraversalError]] encapsulating it 16 | * will be thrown when asking for traversal result. 17 | * 18 | * @see [[TraversalFusion]] 19 | */ 20 | trait Traversal { 21 | protected[traversal] val universe: scala.reflect.api.Universe 22 | import universe._ 23 | 24 | /** 25 | * Step function that modifies internal traversal state. 26 | * 27 | * Must be a value member of the [[Traversal]] class since the optimizer macro 28 | * needs to inject class discoveries somewhere (ie. it adds members to PartialFunction subtype) 29 | */ 30 | val step: PartialFunction[Tree, Unit] 31 | 32 | /** Errors discovered during traversal */ 33 | case class TraversalError(pos: Position, cause: Throwable) extends RuntimeException(cause) { 34 | def message = s"Error occured during traversal. Cause: ${cause.getMessage}\nUse -Ydebug to view stacktrace" 35 | } 36 | 37 | private var _error: Option[TraversalError] = None 38 | 39 | /** 40 | * Safely performs stepping and deals with issues in traverser (like exception throwing) 41 | * 42 | * If an error has been thrown previously, ignore traversal (state doesn't make sense once a [[step]] has 43 | * failed). 44 | * 45 | * @return whether we actually applied a step (basically when step.isDefinedAt(tree) is true) 46 | */ 47 | private[traversal] def apply(tree: Tree): Boolean = { 48 | if (_error.isDefined) false else try { 49 | if (!step.isDefinedAt(tree)) false else { 50 | step.apply(tree) 51 | true 52 | } 53 | } 54 | catch { 55 | case t: Throwable => 56 | _error = Some(TraversalError(tree.pos, t)) 57 | false 58 | } 59 | } 60 | 61 | /** 62 | * State type that is trundled along during traversal (no constraints since 63 | * state updates will actually act on the state type). 64 | * 65 | * @see TraversalStep 66 | * @see FusingTraversals 67 | */ 68 | type State 69 | 70 | /** Initial state value used during traversal */ 71 | def emptyState: State 72 | 73 | private var _state: State = _ 74 | 75 | /** 76 | * Current traversal state. 77 | * 78 | * State is maintained internally to enable foreach traversals. These traversals will use the [[transform]] 79 | * method in each step to update the internal state. Typically, helper methods will be provided for use in the [[step]] 80 | * partial function so [[transform]] doesn't need to be accessed directly. 81 | */ 82 | protected def state: State = { 83 | assert(_state != null, "Attempted to access traversal state before initialization") 84 | _state 85 | } 86 | 87 | /** 88 | * State resulting from a full traversal of a tree. 89 | * 90 | * If an error was encountered during the traversal, this error will be thrown here so the calling code can catch 91 | * it and deal with the error accordingly. 92 | */ 93 | def result: State = if (!_error.isDefined) state else throw _error.get 94 | 95 | /** Updates the internal traversal state by applying function _f_ to the current internal state. */ 96 | protected[traversal] def transform(f: State => State): Unit = { 97 | _state = f(state) 98 | } 99 | 100 | /** 101 | * Sets up traversal after a previous run (or for initial run) basically by copying the [[emptyState]] result to the traversal's 102 | * internal state variable 103 | */ 104 | protected[traversal] def init: Unit = { 105 | _state = emptyState 106 | _error = None 107 | } 108 | 109 | /* Cache only used for debugging, see [[traverse]] */ 110 | private lazy val fused = Fuse(universe)(this.asInstanceOf[Traversal { val universe: Traversal.this.universe.type }]) 111 | 112 | /** Perform traversal directly without fusing, mostly for testing purposes */ 113 | def traverse(tree: Tree): Unit = { 114 | fused.traverse(tree) 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /macros/src/test/scala/scala/reflect/internal/traversal/TraversalTest.scala: -------------------------------------------------------------------------------- 1 | package scala.reflect.internal.traversal 2 | 3 | import scala.reflect.internal.traversal._ 4 | import org.scalatest._ 5 | 6 | class TraversalTest extends FlatSpec with Matchers with TreeProvider { 7 | 8 | object pathTraverser1 extends { 9 | val universe : TraversalTest.this.global.type = TraversalTest.this.global 10 | } with OptimizingTraversal { 11 | import universe._ 12 | 13 | type State = Set[Tree] 14 | def emptyState : State = Set.empty 15 | 16 | def add(tree : Tree) : Unit = transform(_ + tree) 17 | 18 | val step = optimize { 19 | case varDef @ q"$mods var $name : $tpt = $_" if varDef.symbol.owner.isMethod => 20 | add(varDef) 21 | case tree @ q"$rcv = $expr" => 22 | add(tree) 23 | } 24 | } 25 | 26 | def pathTraversal1(tree : global.Tree) : Set[global.Tree] = { 27 | import global._ 28 | 29 | def rec(tree : Tree) : Set[Tree] = tree match { 30 | case tree : ValDef if tree.symbol.isVar && tree.symbol.owner.isMethod => 31 | tree.children.flatMap(rec(_)).toSet + tree 32 | case tree : Assign => 33 | tree.children.flatMap(rec(_)).toSet + tree 34 | case _ => tree.children.flatMap(rec(_)).toSet 35 | } 36 | 37 | rec(tree) 38 | } 39 | 40 | "Traversal completeness on vars and assigns" should "be valid in AddressBook.scala" in { 41 | val tree = fromFile("traversal/AddressBook.scala") 42 | global.ask { () => 43 | pathTraverser1.traverse(tree) 44 | pathTraverser1.result should be (pathTraversal1(tree)) 45 | } 46 | } 47 | 48 | it should "be valid in SimpleInterpreter.scala" in { 49 | val tree = fromFile("traversal/SimpleInterpreter.scala") 50 | global.ask { () => 51 | pathTraverser1.traverse(tree) 52 | pathTraverser1.result should be (pathTraversal1(tree)) 53 | } 54 | } 55 | 56 | object pathTraverser2 extends { 57 | val universe : TraversalTest.this.global.type = TraversalTest.this.global 58 | } with ScopingTraversal with OptimizingTraversal { 59 | import universe._ 60 | 61 | type State = (List[Tree], Set[(Option[Tree], Tree)]) 62 | def emptyState : State = (Nil, Set.empty) 63 | 64 | def add(tree : Tree) : Unit = transform(state => (state._1, state._2 + (state._1.headOption -> tree))) 65 | 66 | def enter(tree : Tree) : Unit = transform(state => (tree :: state._1, state._2), state => state._1 match { 67 | case x :: xs if x == tree => (xs, state._2) 68 | case _ => state 69 | }) 70 | 71 | val step = optimize { 72 | case dd : DefDef => 73 | for (vparams <- dd.vparamss; vparam <- vparams) add(vparam) 74 | enter(dd) 75 | case varDef @ q"$mods var $name : $tpt = $_" if varDef.symbol.owner.isMethod => 76 | add(varDef) 77 | } 78 | } 79 | 80 | def pathTraversal2(tree : global.Tree) : Set[(Option[global.Tree], global.Tree)] = { 81 | import global._ 82 | 83 | def rec(tree : Tree, parent : Option[Tree]) : Set[(Option[Tree], Tree)] = tree match { 84 | case dd : DefDef => 85 | dd.vparamss.flatten.map(p => parent -> p).toSet ++ dd.children.flatMap(rec(_, Some(dd))) 86 | case tree : ValDef if tree.symbol.isVar && tree.symbol.owner.isMethod => 87 | Set(parent -> tree) ++ tree.children.flatMap(rec(_, parent)) 88 | case _ => tree.children.flatMap(rec(_, parent)).toSet 89 | } 90 | 91 | rec(tree, None) 92 | } 93 | 94 | def niceTest(tree : global.Tree) : Unit = { 95 | pathTraverser2.traverse(tree) 96 | val fast = pathTraverser2.result._2 97 | val naive = pathTraversal2(tree) 98 | 99 | def simplify(set: Set[(Option[global.Tree], global.Tree)]) : Set[String] = { 100 | set.map(p => p._1.map(t => t.symbol.toString) + " -> " + p._2.symbol.toString) 101 | } 102 | 103 | val simpleFast = simplify(fast) 104 | val simpleNaive = simplify(naive) 105 | val intersection = simpleFast intersect simpleNaive 106 | val fastError = (simpleFast -- intersection).toList.sorted 107 | val naiveError = (simpleNaive -- intersection).toList.sorted 108 | 109 | assert(fast == naive, fastError.toString + "\n not equal to \n" + naiveError.toString) 110 | } 111 | 112 | "Traversal completeness on scoping" should "be valid in AddressBook.scala" in { 113 | val tree = fromFile("traversal/AddressBook.scala") 114 | global.ask { () => niceTest(tree) } 115 | } 116 | 117 | it should "be valid in SimpleInterpreter.scala" in { 118 | val tree = fromFile("traversal/SimpleInterpreter.scala") 119 | global.ask { () => niceTest(tree) } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /sbt-plugin/src/main/scala/scala/tools/abide/AbideSbtPlugin.scala: -------------------------------------------------------------------------------- 1 | package scala.tools.abide 2 | 3 | import sbt._ 4 | import Keys._ 5 | import scala.language.reflectiveCalls 6 | import scala.reflect.runtime.{ universe => ru } 7 | 8 | object AbideSbtPlugin extends AutoPlugin { 9 | 10 | object autoImport { 11 | val abide = taskKey[Unit]("Runs abide verification on current project") 12 | } 13 | 14 | import autoImport._ 15 | 16 | private val AbideConfig = config("abide") 17 | 18 | private def abideScalaVersion(version: String): String = CrossVersion.partialVersion(version) match { 19 | case Some((2, scalaMajor)) if scalaMajor >= 11 => version 20 | case Some((2, 10)) => "2.11.1" 21 | } 22 | 23 | private def abideScalaBinaryVersion(version: String): String = CrossVersion.partialVersion(version) match { 24 | case Some((2, scalaMajor)) if scalaMajor >= 11 => s"2.$scalaMajor" 25 | case Some((2, 10)) => "2.11" 26 | } 27 | 28 | private def abideBinaryVersion(version: String): String = s"abide_${abideScalaBinaryVersion(version)}" 29 | 30 | private lazy val abideSettings: Seq[sbt.Def.Setting[_]] = Seq( 31 | ivyConfigurations += AbideConfig, 32 | libraryDependencies ++= Seq( 33 | "com.lightbend" % abideBinaryVersion((scalaVersion in Compile).value) % "0.1-SNAPSHOT" % AbideConfig.name, 34 | "org.scala-lang" % "scala-compiler" % abideScalaVersion((scalaVersion in Compile).value) % AbideConfig.name, 35 | "org.scala-lang" % "scala-library" % abideScalaVersion((scalaVersion in Compile).value) % AbideConfig.name, 36 | "org.scala-lang" % "scala-reflect" % abideScalaVersion((scalaVersion in Compile).value) % AbideConfig.name 37 | ), 38 | abide := { 39 | streams.value.log.info("Running abide in " + thisProject.value.id + " ...") 40 | 41 | val cpOpts: Seq[String] = { 42 | val deps = (dependencyClasspath in Compile).value 43 | val cpString = deps.files.map(_.getAbsolutePath).mkString(java.io.File.pathSeparatorChar.toString) 44 | Seq("-classpath", cpString) 45 | } 46 | 47 | val sourcePaths: Seq[String] = (sources in Compile).value.map(_.getAbsolutePath) 48 | 49 | if (sourcePaths.filter(_.endsWith(".scala")).nonEmpty) { 50 | val abideCp: Seq[java.io.File] = update.value.select(configurationFilter("abide")) 51 | streams.value.log.debug("AbideCp: " + abideCp.mkString("\n")) 52 | 53 | val (ruleClasses, analyzerClasses, presenterClasses) = { 54 | var rules: Seq[String] = Seq.empty 55 | var analyzers: Seq[String] = Seq.empty 56 | var presenters: Seq[String] = Seq.empty 57 | 58 | for (file <- abideCp) { 59 | val pluginXmlStream = sbt.classpath.ClasspathUtilities.toLoader(Seq(file)).getResourceAsStream("abide-plugin.xml") 60 | 61 | if (pluginXmlStream != null) scala.xml.XML.load(pluginXmlStream) match { 62 | case { elems @ _* } => elems.foreach { 63 | case rule @ => rules :+= (rule \ "@class").text 64 | case analyzer @ => analyzers :+= (analyzer \ "@class").text 65 | case presenter @ => presenters :+= (presenter \ "@class").text 66 | case _ => () 67 | } 68 | case _ => () 69 | } 70 | } 71 | 72 | (rules, analyzers, presenters) 73 | } 74 | 75 | val ruleOpts = ruleClasses.map(cls => "-P:abide:ruleClass:" + cls) 76 | val analyzerOpts = analyzerClasses.map(cls => "-P:abide:analyzerClass:" + cls) 77 | val presenterOpts = presenterClasses.map(cls => "-P:abide:presenterClass:" + cls) 78 | 79 | val options = cpOpts ++ ruleOpts ++ analyzerOpts ++ presenterOpts ++ sourcePaths 80 | 81 | streams.value.log.debug(options.mkString("\n")) 82 | 83 | val loader: ClassLoader = sbt.classpath.ClasspathUtilities.toLoader(abideCp) 84 | 85 | val mirror = ru.runtimeMirror(loader) 86 | val objectSymbol = mirror.staticModule("scala.tools.abide.Abide") 87 | val abideObj = mirror.reflectModule(objectSymbol).instance.asInstanceOf[{ def main(args: Array[String]): Unit }] 88 | 89 | val nRules = ruleClasses.size 90 | 91 | if (nRules > 0) { 92 | streams.value.log.info(s"Checking $nRules abide rules") 93 | abideObj.main(options.toArray) 94 | } 95 | else 96 | streams.value.log.info(s"No abide rules found. Maybe you forgot to add dependencies on rule packages?") 97 | } 98 | else { 99 | streams.value.log.info("No scala sources : skipping project.") 100 | } 101 | } 102 | ) 103 | 104 | override def requires = sbt.plugins.JvmPlugin 105 | 106 | override def trigger = allRequirements 107 | 108 | override def projectSettings = super.projectSettings ++ abideSettings 109 | 110 | } 111 | -------------------------------------------------------------------------------- /rules/core/src/main/scala/com/lightbend/abide/core/Inaccessible.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide._ 4 | import scala.tools.abide.traversal._ 5 | 6 | class Inaccessible(val context: Context) extends WarningRule { 7 | import context.universe._ 8 | 9 | val name = "inaccessible" 10 | 11 | case class Warning(val pos: Position, methodName: Name, className: Name, inaccessibleClassName: Name) extends RuleWarning { 12 | val message = 13 | s"""Method $methodName in class $className references private class $inaccessibleClassName. 14 | |Classes which cannot access $inaccessibleClassName may be unable to override $methodName""".stripMargin 15 | } 16 | 17 | implicit class SymbolOps(sym: Symbol) { 18 | def isModuleOrModuleClass = sym.isModule || sym.isModuleClass 19 | 20 | def isTopLevel = sym.owner.isPackageClass 21 | 22 | def isLocalToBlock: Boolean = sym.owner.isTerm 23 | 24 | def isEffectivelyFinal: Boolean = ( 25 | sym.isFinal 26 | || sym.hasPackageFlag 27 | || isModuleOrModuleClass && isTopLevel 28 | || sym.isTerm && (sym.isPrivate || isLocalToBlock) 29 | ) 30 | 31 | def isNotOverridden = { 32 | if (sym.owner.isClass) { 33 | val classOwner = sym.owner.asClass 34 | if (classOwner.isSealed) { 35 | classOwner.knownDirectSubclasses.forall(c => { 36 | c.isEffectivelyFinal && c.typeSignature.declarations.exists(subSym => subSym.allOverriddenSymbols.contains(sym)) 37 | }) 38 | } 39 | else classOwner.isEffectivelyFinal 40 | } 41 | else false 42 | } 43 | 44 | def isDeferred = (sym.flags & Flag.DEFERRED) != 0 45 | 46 | def isEffectivelyFinalOrNotOverridden: Boolean = { 47 | isEffectivelyFinal || (sym.isTerm && !isDeferred && isNotOverridden) 48 | } 49 | 50 | def isLessAccessibleThan(other: Symbol): Boolean = { 51 | val tb = sym.accessBoundary(sym.owner) 52 | val ob1 = other.accessBoundary(sym.owner) 53 | val ob2 = ob1.linkedClassOfClass 54 | var o = tb 55 | while (o != NoSymbol && o != ob1 && o != ob2) { 56 | o = o.owner 57 | } 58 | o != NoSymbol && o != tb 59 | } 60 | 61 | def hasAccessBoundary = sym.privateWithin != NoSymbol 62 | 63 | def accessBoundary(base: Symbol): Symbol = { 64 | def enclosingRootClass(s: Symbol): Symbol = s match { 65 | case s: ClassSymbol => s 66 | case _ => enclosingRootClass(s.owner) 67 | } 68 | if (sym.isPrivate || sym.isLocalToBlock) sym.owner 69 | else if (sym.isProtected && sym.isStatic && sym.isJava) enclosingRootClass(sym) 70 | else if (hasAccessBoundary) sym.privateWithin 71 | else if (sym.isProtected) base 72 | else enclosingRootClass(sym) 73 | } 74 | } 75 | 76 | val step = optimize { 77 | case defDef @ DefDef(_, name, _, _, _, _) if !defDef.symbol.isSynthetic => 78 | val member = defDef.symbol 79 | val isConstructor = member.isMethod && member.asMethod.isConstructor 80 | if (!isConstructor && !member.isEffectivelyFinalOrNotOverridden) { 81 | 82 | def checkAccessibilityOfType(tpe: Type) = { 83 | val inaccessible = lessAccessibleSymsInType(tpe, member) 84 | // If the unnormalized type is accessible, that's good enough 85 | if (inaccessible.isEmpty) () 86 | // Or if the normalized type is, that's good too 87 | else if ((tpe ne tpe.normalize) && lessAccessibleSymsInType(tpe.dealiasWiden, member).isEmpty) () 88 | // Otherwise warn about the inaccessible syms in the unnormalized type 89 | else inaccessible foreach (sym => { 90 | nok(Warning(defDef.pos, name, sym.owner.name, sym.name)) 91 | }) 92 | } 93 | 94 | // Types of the value parameters 95 | mapParamss(member)(p => checkAccessibilityOfType(p.tpe)) 96 | // Upper bounds of type parameters 97 | member.typeParams.map(_.info.bounds.hi.widen) foreach checkAccessibilityOfType 98 | } 99 | 100 | def lessAccessibleSymsInType(other: Type, memberSym: Symbol): List[Symbol] = { 101 | val extras = other match { 102 | case TypeRef(pre, _, args) => 103 | // Checking the prefix here gives us spurious errors on e.g. a 104 | // private[process] object which contains a type alias, which 105 | // normalizes to a visible type. 106 | args filterNot (_ eq NoPrefix) flatMap (tp => lessAccessibleSymsInType(tp, memberSym)) 107 | case _ => Nil 108 | } 109 | if (lessAccessible(other.typeSymbol, memberSym)) other.typeSymbol :: extras 110 | else extras 111 | } 112 | 113 | def lessAccessible(otherSym: Symbol, memberSym: Symbol): Boolean = ( 114 | (otherSym != NoSymbol) 115 | && !otherSym.isProtected 116 | && !otherSym.isTypeParameterOrSkolem 117 | && !otherSym.isExistentiallyBound 118 | && (otherSym isLessAccessibleThan memberSym) 119 | && (otherSym isLessAccessibleThan memberSym.enclClass) 120 | ) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /rules/core/src/test/scala/com/lightbend/abide/core/UnusedMemberTest.scala: -------------------------------------------------------------------------------- 1 | package com.lightbend.abide.core 2 | 3 | import scala.tools.abide.traversal._ 4 | import com.lightbend.abide.core._ 5 | 6 | class UnusedMemberTest extends TraversalTest { 7 | 8 | val rule = new UnusedMember(context) 9 | 10 | "Unused local members" should "be discovered when simple" in { 11 | val tree = fromString(""" 12 | class Toto { 13 | private val titi = 2 14 | } 15 | """) 16 | 17 | global.ask { () => apply(rule)(tree).size should be(1) } 18 | } 19 | 20 | it should "be discovered when private[this]" in { 21 | val tree = fromString(""" 22 | class Toto { 23 | private[this] val titi = 2 24 | } 25 | """) 26 | 27 | global.ask { () => apply(rule)(tree).size should be(1) } 28 | } 29 | 30 | it should "be discovered when `def`" in { 31 | val tree = fromString(""" 32 | class Toto { 33 | private def titi = 0 34 | } 35 | """) 36 | 37 | global.ask { () => apply(rule)(tree).size should be(1) } 38 | } 39 | 40 | // can't actually be found in the compiler plugin because these values will be 41 | // folded by the type-checker (and therefore won't be used elsewhere), so we ignore them 42 | ignore should "be discovered when private final" in { 43 | val tree = fromString(""" 44 | class Toto { 45 | private final val titi = 0 46 | } 47 | """) 48 | 49 | global.ask { () => apply(rule)(tree).size should be(1) } 50 | } 51 | 52 | it should "be ignored when synthetic" in { 53 | val tree = fromString(""" 54 | class Toto { 55 | def test(list : List[Int]) : List[Int] = list.map(_ => 1) 56 | } 57 | """) 58 | 59 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 60 | } 61 | 62 | it should "be ignored when used in super-constructor" in { 63 | val tree = fromString(""" 64 | class Foo(val a : Int) 65 | class Bar(b : Int) extends Foo(b) 66 | """) 67 | 68 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 69 | } 70 | 71 | it should "be reported when NOT used in super-constructor" in { 72 | val tree = fromString(""" 73 | class Foo(val a : Int) 74 | class Bar(b : Int) extends Foo(1) 75 | """) 76 | 77 | global.ask { () => apply(rule)(tree).size should be(1) } 78 | } 79 | 80 | it should "be ignored when used in case statements" in { 81 | val tree = fromString(""" 82 | class Toto { 83 | def test(l : List[Int]) : Int = l match { 84 | case x :: _ => x 85 | case _ => 0 86 | } 87 | } 88 | """) 89 | 90 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 91 | } 92 | 93 | it should "be reported when NOT used in rhs of case statement" in { 94 | val tree = fromString(""" 95 | class Toto { 96 | def test(l : List[Int]) : Int = l match { 97 | case x :: xs => x 98 | case a => 0 99 | } 100 | } 101 | """) 102 | 103 | global.ask { () => apply(rule)(tree).size should be(2) } 104 | } 105 | 106 | it should "be found in Properties.scala" in { 107 | val tree = fromFile("Properties.scala") 108 | global.ask { () => apply(rule)(tree).size should be(1) } 109 | } 110 | 111 | "Other members" should "be valid when used" in { 112 | val tree = fromString(""" 113 | class Toto { 114 | private def titi = 0 115 | println(titi) 116 | } 117 | """) 118 | 119 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 120 | } 121 | 122 | it should "be valid when private final is used" in { 123 | val tree = fromString(""" 124 | class Toto { 125 | final private val test = 0 126 | def toto = test + 1 127 | } 128 | """) 129 | 130 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 131 | } 132 | 133 | it should "be valid when private final is used in objects" in { 134 | val tree = fromString(""" 135 | object Titi { 136 | final private val test = 0 137 | } 138 | class Titi { 139 | import Titi._ 140 | def toto = test + 1 141 | } 142 | """) 143 | 144 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 145 | } 146 | 147 | it should "be valid when public" in { 148 | val tree = fromString(""" 149 | class Toto { 150 | val tree = 0 151 | } 152 | """) 153 | 154 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 155 | } 156 | 157 | it should "be valid when package-private" in { 158 | val tree = fromString(""" 159 | package toto 160 | class Toto { 161 | private[toto] val tree = 0 162 | } 163 | """) 164 | 165 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 166 | } 167 | 168 | it should "be valid in MurmurHash.scala" in { 169 | val tree = fromFile("MurmurHash.scala") 170 | global.ask { () => apply(rule)(tree).isEmpty should be(true) } 171 | } 172 | 173 | it should "be valid in MurmurHash.scala alongside other rules" in { 174 | val tree = fromFile("MurmurHash.scala") 175 | val rules = List( 176 | rule, 177 | new MatchCaseOnSeq(context), 178 | new PublicMutable(context), 179 | new RenamedDefaultParameter(context), 180 | new StupidRecursion(context), 181 | new LocalValInsteadOfVar(context), 182 | new MemberValInsteadOfVar(context) 183 | ) 184 | global.ask { () => apply(rules: _*)(tree).isEmpty should be(true) } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /macros/src/test/scala/scala/reflect/internal/traversal/SpeedAnalysis.scala: -------------------------------------------------------------------------------- 1 | package scala.reflect.internal.traversal 2 | 3 | import scala.reflect.internal.traversal._ 4 | import org.scalatest.FunSuite 5 | 6 | class SpeedAnalysis extends FunSuite with TreeProvider { 7 | import global._ 8 | import org.scalatest._ 9 | 10 | val processorCount = 100 11 | 12 | def speed(name : String, tree : Tree, traverser : Tree => List[Symbol]) : Long = { 13 | val start = System.currentTimeMillis 14 | val warnings = try { 15 | traverser(tree) 16 | } catch { 17 | case t : Throwable => 18 | alert("misshap during traversal : " + t.getMessage) 19 | Nil 20 | } 21 | val size = warnings.size 22 | val time = System.currentTimeMillis - start 23 | info("name: " + name + " -> time="+time + ", warnings="+ size) 24 | time 25 | } 26 | 27 | class FastTraversalImpl extends OptimizingTraversal { 28 | override val universe : SpeedAnalysis.this.global.type = SpeedAnalysis.this.global 29 | import universe._ 30 | 31 | type State = Map[Symbol, Boolean] 32 | def emptyState : State = Map.empty 33 | 34 | val step = optimize { 35 | case varDef @ q"$mods var $name : $tpt = $_" if varDef.symbol.owner.isMethod => 36 | transform(state => state + (varDef.symbol -> state.getOrElse(varDef.symbol, false))) 37 | case q"$rcv = $expr" => 38 | transform(_ + (rcv.symbol -> true)) 39 | } 40 | } 41 | 42 | val fastTraverser = Fuse(global)((1 to processorCount).map { x => new FastTraversalImpl } : _*).force 43 | 44 | def traverseFast(tree : Tree) : List[Symbol] = { 45 | fastTraverser.traverse(tree) 46 | fastTraverser.traversals.toList.flatMap { 47 | x => x.result.asInstanceOf[Map[Symbol, Boolean]].collect { case (a, false) => a } 48 | } 49 | } 50 | 51 | def naiveTraverser(tree : Tree) : Map[Symbol, Boolean] = tree match { 52 | case varDef @ q"$mods var $name : $tpt = $_" if varDef.symbol.owner.isMethod => 53 | varDef.children.map(naiveTraverser(_)).foldLeft(Map(varDef.symbol -> false)) { (all, next) => 54 | (all.keys ++ next.keys).map(k => k -> (all.getOrElse(k, false) || next.getOrElse(k, false))).toMap 55 | } 56 | case assign @ q"$rcv = $expr" => 57 | assign.children.map(naiveTraverser(_)).foldLeft(Map(rcv.symbol -> true)) { (all, next) => 58 | (all.keys ++ next.keys).map(k => k -> (all.getOrElse(k, false) || next.getOrElse(k, false))).toMap 59 | } 60 | case tree => 61 | tree.children.map(naiveTraverser(_)).foldLeft(Map.empty[Symbol,Boolean]) { (all, next) => 62 | (all.keys ++ next.keys).map(k => k -> (all.getOrElse(k, false) || next.getOrElse(k, false))).toMap 63 | } 64 | } 65 | 66 | def traverseNaive(tree : Tree) : List[Symbol] = { 67 | (1 to processorCount).flatMap(x => naiveTraverser(tree).toSeq).collect { case (a,false) => a }.toList 68 | } 69 | 70 | case class ValidationState(map : Map[Symbol, Boolean]) { 71 | val issues : List[Symbol] = map.filter(!_._2).map(_._1).toList 72 | 73 | def merge(that : ValidationState) : ValidationState = { 74 | def merged(k : Symbol) = map.getOrElse(k, false) || that.map.getOrElse(k, false) 75 | ValidationState((map.keys ++ that.map.keys).map(k => k -> merged(k)).toMap) 76 | } 77 | } 78 | 79 | def naiveStatefulTraverser(tree : Tree) : ValidationState = tree match { 80 | case varDef @ q"$mods var $name : $tpt = $_" if varDef.symbol.owner.isMethod => 81 | varDef.children.map(naiveStatefulTraverser(_)).foldLeft(ValidationState(Map(varDef.symbol -> false))) { (all, next) => 82 | all merge next 83 | } 84 | case assign @ q"$rcv = $expr" => 85 | assign.children.map(naiveStatefulTraverser(_)).foldLeft(ValidationState(Map(rcv.symbol -> true))) { (all, next) => 86 | all merge next 87 | } 88 | case tree => 89 | tree.children.map(naiveStatefulTraverser(_)).foldLeft(ValidationState(Map.empty[Symbol,Boolean])) { (all, next) => 90 | all merge next 91 | } 92 | } 93 | 94 | def traverseStatefulNaive(tree : Tree) : List[Symbol] = { 95 | (1 to processorCount).flatMap(x => naiveStatefulTraverser(tree).issues).toList 96 | } 97 | 98 | /* 99 | // initialize traversals, they seem to be slow on first run sometimes... 100 | val tree = fromFile("traversal/AddressBook.scala") 101 | global.ask { () => 102 | traverseFast(tree) 103 | traverseNaive(tree) 104 | traverseStatefulNaive(tree) 105 | } 106 | */ 107 | 108 | ignore("Fast traversal is fast in AddressBook.scala") { 109 | val tree = fromFile("traversal/AddressBook.scala") 110 | global.ask { () => 111 | val fastTime = speed("fast", tree, traverseFast) 112 | val naiveTime = speed("naive", tree, traverseNaive) 113 | val statefulNaiveTime = speed("naiveState", tree, traverseStatefulNaive) 114 | 115 | assert(fastTime < naiveTime / 2, "Fusing should make simple rules at least faster") 116 | assert(fastTime < statefulNaiveTime / 2, "Fusing should make simple rules at least faster, also against stateful approach") 117 | } 118 | } 119 | 120 | ignore("Fast traversal is fast in SimpleInterpreter.scala") { 121 | val tree = fromFile("traversal/SimpleInterpreter.scala") 122 | global.ask { () => 123 | val fastTime = speed("fast", tree, traverseFast) 124 | val naiveTime = speed("naive", tree, traverseNaive) 125 | val statefulNaiveTime = speed("naiveState", tree, traverseStatefulNaive) 126 | 127 | assert(fastTime < naiveTime / 2, "Fusing should make simple rules at least faster") 128 | assert(fastTime < statefulNaiveTime / 2, "Fusing should make simple rules at least faster, also against stateful approach") 129 | } 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /wiki/extensions.md: -------------------------------------------------------------------------------- 1 | # Abide Extensions 2 | 3 | The **abide** framework was build to be easily extensible, and not only with respect to [rule addition](/wiki/rules.md). One can also extend two other key features of the framework, however doing so is slightly more involved as rule contribution. 4 | 5 | ### Adding new directives 6 | 7 | Directives are simply traits that will be mixed in to the rule context at rule instantiation. By default, a simple 8 | `Context` value will be used, but this behavior can be overriden by defining a companion object to the rule that 9 | extends the `ContextGenerator` trait. These generators will instantiate the correct context for the companion rule and the **abide** compiler plugin will generalise the context in order to share cache. 10 | 11 | Concretely, given a generator that creates `Context` typed contexts and another that generates `C <: Context`, all rules will share the context instance with type `C` since it generalises `Context`. This enables cache sharing without requiring knowledge of existing rules and context types. 12 | 13 | To bind the the type `C <: Context` to the rule that will be consuming this context, we define a companion object 14 | ```scala 15 | object SomeRule extends ContextGenerator { 16 | // C is typically a mixin of Context and some other stuff 17 | def getContext(universe : SymbolTable) : C = new C 18 | } 19 | 20 | class SomeRule(val context : C) extends Rule { ... } 21 | ```` 22 | and the **abide** compiler plugin will automatically share the `C` typed context between all rules for which it remains 23 | valid. 24 | 25 | Note that any `SomeRule` subtype will also require a `C` typed context even if `SomeRule` is not actually used in the 26 | verification run, and the framework will indeed instantiate a `C` context to automatically guarantee type safety. 27 | 28 | ### Defining analyzers 29 | 30 | In order to perform rule verification, **abide** relies on `Analyzer` instances that know how to actually apply a rule to source trees. For example, in the case of `TraversalRule` classes, the `FusingTraversalAnalyzer` will fuse the rules together before applying traversal to increase performance. This lets the analyzer optimize for global information that isn't available inside a particular rule. 31 | 32 | When declaring a rule, an analyzer type has to be attached to it so the framework will know how to actually apply the rule. This is managed by the `val analyzer : AnalyzerGenerator` field contained in rules. An `AnalyzerGenerator` is an object that knows how to instantiate an `Analyzer` given a `Context` and a set of rules (the rules that share this generator object). 33 | 34 | Since the analyzer attached to a rule is statically determined by the `analyzer` field, we need a mechanism to inject new (and possibly more powerful) analyzers. For example, say we have the generator 35 | ```scala 36 | object SomeAnalyzerGenerator extends AnalyzerGenerator { 37 | def generateAnalyzer(universe : SymbolTable, rules : List[Rule]) : Analyzer = 38 | new SomeAnalyzer(universe, rules) 39 | } 40 | ``` 41 | and someone comes along with a new analyzer that should replace `SomeAnalyzer`, but doesn't want to change the rule definitions (where the `analyzer` field is specified) for some reason. It suffices to declare in the new generator that it subsumes `SomeAnalyzerGenerator`. 42 | ```scala 43 | object SomeOtherAnalyzerGenerator extends AnalyzerGenerator { 44 | def generateAnalyzer(universe : SymbolTable, rules : List[Rule]) : Analyzer = 45 | new SomeOtherAnalyzer(universe, rules) 46 | val subsumes : Set[AnalyzerGenerator] = Set(SomeAnalyzerGenerator) 47 | } 48 | ``` 49 | 50 | Any rule whose generator is transitively subsumed by another generator will be assigned to the analyzer generated by the later. This enables users to extend the framework with only analyzer plugins that will be automatically used to perform analysis by the compiler plugin. 51 | 52 | In order to specify to the framework which generators are provided by a package, analyzer generator classes must be appended to the abide-plugin.xml descriptor in an `` element. 53 | 54 | For a concrete example, see [NaiveTraversalAnalyzer](/abide/src/main/scala/scala/tools/abide/traversal/NaiveTraversalAnalyzer.scala) which is subsumed by [FusingTraversalAnalyzer](/abide/src/main/scala/scala/tools/abide/traversal/FusingTraversalAnalyzer.scala) for traversal rules. 55 | 56 | As of now, only the analyzer for unit-local, flow-agnostic rules has been written, but cross-unit and flow-sensitive 57 | backends should be added in the future (if deemed useful). However, many simple(r) rules do not actually need flow 58 | information and warnings can be collected by a single pass through a compilation unit's body. Such rules are called 59 | _traversal rules_ in the **abide** lingo. See 60 | [writing traversal rules](/wiki/traversal/traversal-rules.md) and 61 | [abide rules](/wiki/rules.md) for more details. 62 | 63 | ### Defining presenters 64 | 65 | In order to output results, **abide** relies on `Presenter` instances that know how to actually process the output of ***abide***. 66 | For example, the `ConsolePresenter` will output the results as compiler messages. 67 | 68 | When declaring a presenter, a generator type has to be attached to it so the framework will know how to actually create the presenter. 69 | A `PresenterGenerator` is an object that knows how to instantiate an `Presenter` given all the needed context and for it to run. 70 | 71 | ```scala 72 | object SomePresenterGenerator extends PresenterGenerator { 73 | def generatePresenter(global: Global) : Presenter = 74 | new SomePresenter(global) 75 | } 76 | ``` 77 | 78 | ```scala 79 | class SomePresenter(protected val global: Global) extends Presenter { 80 | 81 | import global._ 82 | 83 | def apply(unit: CompilationUnit, warnings: List[Warning]): Unit = { 84 | // TODO: implement the presenter 85 | } 86 | } 87 | ``` 88 | -------------------------------------------------------------------------------- /rules/core/src/test/resources/MurmurHash.scala: -------------------------------------------------------------------------------- 1 | /* __ *\ 2 | ** ________ ___ / / ___ Scala API ** 3 | ** / __/ __// _ | / / / _ | (c) 2003-2013, LAMP/EPFL ** 4 | ** __\ \/ /__/ __ |/ /__/ __ | http://scala-lang.org/ ** 5 | ** /____/\___/_/ |_/____/_/ | | ** 6 | ** |/ ** 7 | \* */ 8 | 9 | package scala 10 | package util 11 | 12 | /** An implementation of Austin Appleby's MurmurHash 3.0 algorithm 13 | * (32 bit version); reference: http://code.google.com/p/smhasher 14 | * 15 | * This is the hash used by collections and case classes (including 16 | * tuples). 17 | * 18 | * @author Rex Kerr 19 | * @version 2.9 20 | * @since 2.9 21 | */ 22 | 23 | import java.lang.Integer.{ rotateLeft => rotl } 24 | import scala.collection.Iterator 25 | 26 | /** A class designed to generate well-distributed non-cryptographic 27 | * hashes. It is designed to be passed to a collection's foreach method, 28 | * or can take individual hash values with append. Its own hash code is 29 | * set equal to the hash code of whatever it is hashing. 30 | */ 31 | @deprecated("Use the object MurmurHash3 instead.", "2.10.0") 32 | class MurmurHash[@specialized(Int,Long,Float,Double) T](seed: Int) extends (T => Unit) { 33 | import MurmurHash._ 34 | 35 | private var h = startHash(seed) 36 | private var c = hiddenMagicA 37 | private var k = hiddenMagicB 38 | private var hashed = false 39 | private var hashvalue = h 40 | 41 | /** Begin a new hash using the same seed. */ 42 | def reset() { 43 | h = startHash(seed) 44 | c = hiddenMagicA 45 | k = hiddenMagicB 46 | hashed = false 47 | } 48 | 49 | /** Incorporate the hash value of one item. */ 50 | def apply(t: T) { 51 | h = extendHash(h,t.##,c,k) 52 | c = nextMagicA(c) 53 | k = nextMagicB(k) 54 | hashed = false 55 | } 56 | 57 | /** Incorporate a known hash value. */ 58 | def append(i: Int) { 59 | h = extendHash(h,i,c,k) 60 | c = nextMagicA(c) 61 | k = nextMagicB(k) 62 | hashed = false 63 | } 64 | 65 | /** Retrieve the hash value */ 66 | def hash = { 67 | if (!hashed) { 68 | hashvalue = finalizeHash(h) 69 | hashed = true 70 | } 71 | hashvalue 72 | } 73 | override def hashCode = hash 74 | } 75 | 76 | /** An object designed to generate well-distributed non-cryptographic 77 | * hashes. It is designed to hash a collection of integers; along with 78 | * the integers to hash, it generates two magic streams of integers to 79 | * increase the distribution of repetitive input sequences. Thus, 80 | * three methods need to be called at each step (to start and to 81 | * incorporate a new integer) to update the values. Only one method 82 | * needs to be called to finalize the hash. 83 | */ 84 | @deprecated("Use the object MurmurHash3 instead.", "2.10.0") 85 | // NOTE: Used by SBT 0.13.0-M2 and below 86 | object MurmurHash { 87 | // Magic values used for MurmurHash's 32 bit hash. 88 | // Don't change these without consulting a hashing expert! 89 | final private val visibleMagic = 0x971e137b 90 | final private val hiddenMagicA = 0x95543787 91 | final private val hiddenMagicB = 0x2ad7eb25 92 | final private val visibleMixer = 0x52dce729 93 | final private val hiddenMixerA = 0x7b7d159c 94 | final private val hiddenMixerB = 0x6bce6396 95 | final private val finalMixer1 = 0x85ebca6b 96 | final private val finalMixer2 = 0xc2b2ae35 97 | 98 | // Arbitrary values used for hashing certain classes 99 | final private val seedString = 0xf7ca7fd2 100 | final private val seedArray = 0x3c074a61 101 | 102 | /** The first 23 magic integers from the first stream are stored here */ 103 | val storedMagicA = 104 | Iterator.iterate(hiddenMagicA)(nextMagicA).take(23).toArray 105 | 106 | /** The first 23 magic integers from the second stream are stored here */ 107 | val storedMagicB = 108 | Iterator.iterate(hiddenMagicB)(nextMagicB).take(23).toArray 109 | 110 | /** Begin a new hash with a seed value. */ 111 | def startHash(seed: Int) = seed ^ visibleMagic 112 | 113 | /** The initial magic integers in the first stream. */ 114 | def startMagicA = hiddenMagicA 115 | 116 | /** The initial magic integer in the second stream. */ 117 | def startMagicB = hiddenMagicB 118 | 119 | /** Incorporates a new value into an existing hash. 120 | * 121 | * @param hash the prior hash value 122 | * @param value the new value to incorporate 123 | * @param magicA a magic integer from the stream 124 | * @param magicB a magic integer from a different stream 125 | * @return the updated hash value 126 | */ 127 | def extendHash(hash: Int, value: Int, magicA: Int, magicB: Int) = { 128 | (hash ^ rotl(value*magicA,11)*magicB)*3 + visibleMixer 129 | } 130 | 131 | /** Given a magic integer from the first stream, compute the next */ 132 | def nextMagicA(magicA: Int) = magicA*5 + hiddenMixerA 133 | 134 | /** Given a magic integer from the second stream, compute the next */ 135 | def nextMagicB(magicB: Int) = magicB*5 + hiddenMixerB 136 | 137 | /** Once all hashes have been incorporated, this performs a final mixing */ 138 | def finalizeHash(hash: Int) = { 139 | var i = (hash ^ (hash>>>16)) 140 | i *= finalMixer1 141 | i ^= (i >>> 13) 142 | i *= finalMixer2 143 | i ^= (i >>> 16) 144 | i 145 | } 146 | 147 | /** Compute a high-quality hash of an array */ 148 | def arrayHash[@specialized T](a: Array[T]) = { 149 | var h = startHash(a.length * seedArray) 150 | var c = hiddenMagicA 151 | var k = hiddenMagicB 152 | var j = 0 153 | while (j < a.length) { 154 | h = extendHash(h, a(j).##, c, k) 155 | c = nextMagicA(c) 156 | k = nextMagicB(k) 157 | j += 1 158 | } 159 | finalizeHash(h) 160 | } 161 | 162 | /** Compute a high-quality hash of a string */ 163 | def stringHash(s: String) = { 164 | var h = startHash(s.length * seedString) 165 | var c = hiddenMagicA 166 | var k = hiddenMagicB 167 | var j = 0 168 | while (j+1 < s.length) { 169 | val i = (s.charAt(j)<<16) + s.charAt(j+1) 170 | h = extendHash(h,i,c,k) 171 | c = nextMagicA(c) 172 | k = nextMagicB(k) 173 | j += 2 174 | } 175 | if (j < s.length) h = extendHash(h,s.charAt(j).toInt,c,k) 176 | finalizeHash(h) 177 | } 178 | 179 | /** Compute a hash that is symmetric in its arguments--that is, 180 | * where the order of appearance of elements does not matter. 181 | * This is useful for hashing sets, for example. 182 | */ 183 | def symmetricHash[T](xs: scala.collection.TraversableOnce[T], seed: Int) = { 184 | var a,b,n = 0 185 | var c = 1 186 | xs.seq.foreach(i => { 187 | val h = i.## 188 | a += h 189 | b ^= h 190 | if (h != 0) c *= h 191 | n += 1 192 | }) 193 | var h = startHash(seed * n) 194 | h = extendHash(h, a, storedMagicA(0), storedMagicB(0)) 195 | h = extendHash(h, b, storedMagicA(1), storedMagicB(1)) 196 | h = extendHash(h, c, storedMagicA(2), storedMagicB(2)) 197 | finalizeHash(h) 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /README-old.md: -------------------------------------------------------------------------------- 1 | # Abide : Lint tooling for Scala 2 | 3 | **Abide** aims to provide users with a simple framework for lint-like rule creation and verification. 4 | **Abide** rules are designed as small, self-contained units of logic which abstract concerns such as traversal and 5 | optimization away from the rule writter. 6 | 7 | ## Compatibility 8 | 9 | sbt 0.13.12 or later is required. (0.13.13 or later is recommended.) 10 | 11 | Scala 2.11 and 2.12 are supported. 12 | 13 | ## Using the tool 14 | 15 | **Abide** is only available for sbt (and command line) for now, but will be ported to a ScalaIDE plugin and possibly to maven/gradle/etc as well. To add **abide** verification to an sbt project, three different options are available : 16 | 17 | ### Important! 18 | 19 | At this moment, **abide** has not been released. You need to run `sbt publishLocal` in a local checkout of the abide repository. 20 | 21 | ### sbt plugin 22 | 23 | Activate the sbt-abide plugin by adding this to your `project/plugins.sbt` file: 24 | 25 | ```scala 26 | addSbtPlugin("com.lightbend" % "sbt-abide" % "0.1-SNAPSHOT") 27 | ``` 28 | 29 | Now you need to choose the rule libraries by adding the required jars to your dependencies in your project definitions (eg. `build.sbt`). Notice the `abide` configuration at the end of the line. 30 | 31 | ```scala 32 | libraryDependencies += "com.lightbend" %% "abide-core" % "0.1-SNAPSHOT" % "abide" 33 | ``` 34 | 35 | One can also use sbt projects as rule libraries by using `dependsOn(rules % "abide")` in the project definition. This allows one to define project-specific rules! 36 | 37 | To actually run abide, use the `abide` command within sbt. 38 | 39 | ### Compiler plugin 40 | 41 | **Abide** can be activated as a compiler plugin by extending the sbt build file with 42 | 43 | ```scala 44 | libraryDependencies += compilerPlugin("com.lightbend" %% "abide" % "0.1-SNAPSHOT") 45 | scalacOptions ++= Seq( 46 | "-P:abide:abidecp:", 47 | "-P:abide:ruleClass:", 48 | "-P:abide:analyzerClass:", 49 | "-P:abide:presenterClass:", 50 | ...) 51 | ``` 52 | 53 | or simply add these options to your call to `scalac` to enable **abide** during your standard compilation run. 54 | 55 | Since sbt configurations are not available to compiler plugins, the `abidecp` argument is required to specify the 56 | classpath where the **abide** rules are located. In turn, the actual paths to these rules are specified through the 57 | `ruleClass` argument (which will typically appear multiple times, once per rule) and plugin analyzer generators will 58 | appear in `analyzerClass` arguments (also multiple instances). 59 | 60 | While slightly more complex than the following alternative, this mode provides integration capabilities for non-sbt 61 | build tools like Eclipse or maven. 62 | 63 | ### Command line 64 | 65 | The **abide** compiler plugin can also be used directly on the command line by adding the plugin to your `scalac` command and using the options described in the [compiler plugin](#compiler-plugin) as command-line arguments: 66 | ``` 67 | scalac -Xplugin: \ 68 | -P:abide:abidecp: \ 69 | -P:abide:ruleClass: \ 70 | -P:abide:analyzerClass: \ 71 | -P:abide:presenterClass: \ 72 | ... 73 | ``` 74 | 75 | ## Existing plugins 76 | 77 | The **abide** framework comes with a few pre-made rule packages that can be selectively enabled as discussed in the [previous section](#using-the-tool). The list of available packages along with the associated ivy dependency are: 78 | 79 | 1. [rules/core](/wiki/core-rules.md) provided by `"com.lightbend" %% "abide-core" % "0.1-SNAPSHOT"` 80 | 81 | 2. [rules/extra](/wiki/extra-rules.md) provided by `"com.lightbend" %% "abide-extra" % "0.1-SNAPSHOT"` 82 | 83 | 3. [rules/akka](/wiki/akka-rules.md) provided by `"com.lightbend" %% "abide-akka" % "0.1-SNAPSHOT"` 84 | 85 | ## Extending abide 86 | 87 | Setting aside bugfixes and basic feature modification, **abide** components are generally loosely coupled and self-contained to enable simple extensions to the framework. Such extensions will typically fall into one of three different categories and will vary in complexity. From simplest (and most useful extension point) to most involved: 88 | 89 | 1. [rule extension](/wiki/rules.md) 90 | The **abide** framework initially ships with few rules but provides a powerful extension point to accommodate user-defined 91 | rule verification. These rules can either be defined locally (using the `Project(...).dependsOn(rules % "abide")` construction) or shared over github by submitting new rules as pull requests to this repository. 92 | 93 | 2. [directive extension](/wiki/extensions.md#adding-new-directives) 94 | **Abide** rules all share a minimal amount of context, namely the universe instance which defines the compiler AST cake. This context is passed to rules through the `val context : Context` constructor parameter that each rule must define. Directives will typically be mixed in to the shared context object and cache computation results when these can be shared between rules. 95 | 96 | 3. [analyzer extension](/wiki/extensions.md#defining-analyzers) 97 | When rule application can benefit from some global traversal logic, or partial tree analysis, these computations are 98 | performed in an `Analyzer` subtype. These analyzers are typically non-trivial and will be integrated into the main **abide** deliverable to then be shared by all rules. However, analyzers can also be provided alongside rules in plugin 99 | libraries. 100 | 101 | 4. [presenter extension](/wiki/extensions.md#defining-presenters) 102 | The **abide** framework initially ships with one presenter but provides a powerful extension point to accommodate user-defined 103 | presenters. These presenters can either be defined locally (using the `Project(...).dependsOn(rules % "abide")` construction) or shared over github by submitting new presenters as pull requests to this repository. 104 | 105 | The provided extension mechanism uses a plugin architecture based on an xml description that specifies plugin 106 | capabilities. This description sits at the base of the `resources` directory, in `abide-plugin.xml` and has the 107 | following structure: 108 | ```xml 109 | 110 | 111 | 112 | 113 | 114 | 115 | ``` 116 | 117 | Directives don't need to (and shouldn't) be specified in the plugin description as they will be statically referenced 118 | in the source code (and they can't be dynamically subsumed like analyzers). 119 | 120 | ## Further work 121 | 122 | - The **abide** compiler plugin needs to manage rule enabling and disabling. This should be implemented in 123 | `scala.tools.abide.compiler.AbidePlugin` in the `component.apply` method by replacing the analyzer filter (first argument of 124 | ```scala gen(_ => true)(unit.body)```) by an actual filter. 125 | 126 | - Extending build tool support. **Abide** plugins for eclipse, gradle, maven, etc would be a nice feature extension to the 127 | framework. Such extensions should be relatively easy to implement by using the compiler plugin and manually injecting 128 | `-P:abide:...` command line arguments to configure the framework. **Abide** could also be run on the result of a presentation 129 | compiler run, and `scala.tools.abide.compiler.AbidePlugin` would be a good place to start for loading rules, contexts, 130 | generators, etc. One can also look at the tests to witness **abide** verification in action without the compiler plugin. 131 | 132 | - Adding new `Presenter` types. For now, **abide** can only output warnigns as compiler warnings, but it would be nice, for 133 | example, to be able to output interactive HTML5 reports. 134 | 135 | - Enabling cross-unit rules by writing a new analyzer. The base would resemble that of the `FusingTraversalAnalyzer` but 136 | would additionally require keeping track of the result of previous traversals for incremental compilation integration. 137 | -------------------------------------------------------------------------------- /abide/src/main/scala/scala/tools/abide/compiler/AbidePlugin.scala: -------------------------------------------------------------------------------- 1 | package scala.tools.abide.compiler 2 | 3 | import scala.tools.nsc._ 4 | import scala.tools.nsc.plugins._ 5 | import scala.reflect.runtime.{ universe => ru } 6 | 7 | import scala.tools.abide._ 8 | import scala.tools.abide.presentation._ 9 | 10 | /** 11 | * AbidePlugin 12 | * 13 | * Compiler plugin for running the Abide framework. The plugin runs right after `typer` and uses 14 | * a series of rules and analyzers to perform actual verification. It then uses a [[presentation.Presenter]] to 15 | * output the result of the verification. 16 | * 17 | * The plugin accepts a series of abide-specific arguments (use as -P:abide:argName:argValue) : 18 | * 19 | * - ruleClass : 20 | * Specifies the full name of each rule that should be handled by the plugin (eg. com.lightbend.abide.samples.UnusedMember) 21 | * This option can (and should) appear multiple times in the arguments array to specify all rules we're dealing with. 22 | * 23 | * - analyzerClass : 24 | * Specifies the full name of all analyzer generator objects that can be used to instantiate rules. In practice, since the 25 | * analyzer generator is actually a member of the rule, this argument can be omitted. However, the subsumption mechanism 26 | * (see [[AnalyzerGenerator]]) enables users to provide more powerful analyzers that will replace the default analyzer 27 | * statically specified in the class description. Such extension analyzers _must_ appear in an `analyzerClass` argument. 28 | * As in ruleClass, analyzerClass can (and generally should) appear multiple times in the arguments array. 29 | * 30 | * - abidecp : 31 | * Abide can split up rule sets into multiple jars, so when using Abide in compiler plugin mode, these jars need to be 32 | * specified to the compiler so it can actually load the rules. Therefore, we provide the `abidecp` option that must consist 33 | * in a colon-separated list of classpath entries pointing to rule locations. 34 | * 35 | * Note that unlike analyzers, rule context cannot be overriden by arguments and must be specified in the rule's companion object, 36 | * since configurations are custom tailored to single rules (for more information, see [[ContextGenerator]]). 37 | * 38 | * @see [[scala.tools.abide.ContextGenerator]] 39 | * @see [[scala.tools.abide.AnalyzerGenerator]] 40 | */ 41 | class AbidePlugin(val global: Global) extends Plugin { 42 | import global._ 43 | 44 | val name = "abide" 45 | val description = "static code analysis for Scala" 46 | 47 | val components: List[PluginComponent] = List(component) 48 | 49 | private lazy val classLoader = new java.net.URLClassLoader( 50 | abideCp.split(":").filter(_ != "").map(f => new java.io.File(f).toURI.toURL), 51 | getClass.getClassLoader 52 | ) 53 | 54 | private lazy val mirror = ru.runtimeMirror(classLoader) 55 | private lazy val ruleMirrors = for (ruleClass <- ruleClasses) yield { 56 | val ruleSymbol = mirror staticClass ruleClass 57 | val ruleMirror = mirror reflectClass ruleSymbol 58 | 59 | ruleSymbol -> ruleMirror 60 | } 61 | 62 | private lazy val analyzerGenerators = for (analyzerClass <- analyzerClasses) yield { 63 | val analyzerSymbol = mirror staticModule analyzerClass 64 | val analyzerMirror = mirror reflectModule analyzerSymbol 65 | analyzerMirror.instance.asInstanceOf[AnalyzerGenerator] 66 | } 67 | 68 | private lazy val presenterGenerators = for (presenterClass <- presenterClasses) yield { 69 | val presenterSymbol = mirror staticModule presenterClass 70 | val presenterMirror = mirror reflectModule presenterSymbol 71 | presenterMirror.instance.asInstanceOf[PresenterGenerator] 72 | } 73 | 74 | private lazy val ruleContexts = { 75 | import ru._ 76 | 77 | def contextGenerator(sym: ru.Symbol): ru.ModuleSymbol = sym.asClass.baseClasses.collectFirst { 78 | case tpe: ru.TypeSymbol if tpe.toType.companion <:< ru.typeOf[ContextGenerator] => 79 | tpe.toType.companion.typeSymbol.asClass.module.asModule 80 | }.get 81 | 82 | val contextGenerators = for (rule @ (ruleSymbol, ruleMirror) <- ruleMirrors) yield { 83 | val generatorSymbol = contextGenerator(ruleSymbol) 84 | val generatorMirror = mirror reflectModule generatorSymbol 85 | 86 | rule -> generatorMirror.instance.asInstanceOf[ContextGenerator] 87 | } 88 | 89 | def typeTag[T: ru.TypeTag](obj: T): ru.TypeTag[T] = ru.typeTag[T] 90 | def generalize[T1 <: Context: ru.TypeTag, T2 <: Context: ru.TypeTag](o1: T1, o2: T2): Context = { 91 | if (typeTag(o1).tpe <:< typeTag(o2).tpe) o1 else o2 92 | } 93 | 94 | contextGenerators.foldLeft(List.empty[((ru.ClassSymbol, ru.ClassMirror), Context)]) { 95 | case (list, (rule @ (ruleSymbol, ruleMirror), generator)) => 96 | val context = generator.getContext(global) 97 | val bottomCtx = list.foldLeft(context) { case (acc, (rule, ctx)) => generalize(acc, ctx) } 98 | (rule -> bottomCtx) :: (list map { case (rule, ctx) => rule -> generalize(ctx, bottomCtx) }) 99 | }.toMap 100 | } 101 | 102 | private lazy val rules: List[Rule] = for (rule @ (ruleSymbol, ruleMirror) <- ruleMirrors) yield { 103 | val constructorSymbol = ruleSymbol.typeSignature.member(ru.termNames.CONSTRUCTOR).asMethod 104 | val constructorMirror = ruleMirror reflectConstructor constructorSymbol 105 | val context = ruleContexts(rule) 106 | 107 | constructorMirror(context).asInstanceOf[Rule] 108 | } 109 | 110 | private lazy val analyzers: List[(Rule => Boolean) => Analyzer { val global: AbidePlugin.this.global.type }] = { 111 | def generalize(g1: AnalyzerGenerator, g2: AnalyzerGenerator): AnalyzerGenerator = { 112 | def fix[A](a: A)(f: A => A): A = { val na = f(a); if (na == a) na else fix(na)(f) } 113 | val g2Subsumes: Set[AnalyzerGenerator] = fix(g2.subsumes)(set => set ++ set.flatMap(_.subsumes)) 114 | if (g2Subsumes(g1)) g2 else g1 115 | } 116 | 117 | val allGenerators: List[(Rule, AnalyzerGenerator)] = rules.map(rule => rule -> rule.analyzer) 118 | val ruleToGenerator = allGenerators.foldLeft(List.empty[(Rule, AnalyzerGenerator)]) { 119 | case (list, (rule, generator)) => 120 | val generalized = analyzerGenerators.foldLeft(generator)((acc, gen) => generalize(gen, acc)) 121 | val bottomGen = list.foldLeft(generalized) { case (acc, (rule, generator)) => generalize(generator, acc) } 122 | (rule -> bottomGen) :: (list map { case (rule, gen) => rule -> generalize(gen, bottomGen) }) 123 | } 124 | 125 | ruleToGenerator.groupBy(_._2).toList.map { 126 | case (generator, rulePairs) => 127 | val rules = rulePairs.map(_._1) 128 | (filter: Rule => Boolean) => { 129 | val analyzer = generator.getAnalyzer(global, rules.filter(filter)) 130 | analyzer.asInstanceOf[Analyzer { val global: AbidePlugin.this.global.type }] 131 | } 132 | } 133 | } 134 | 135 | private lazy val presenters = { 136 | presenterGenerators.map { generator => 137 | val presenter = generator.getPresenter(global) 138 | presenter.asInstanceOf[Presenter { val global: AbidePlugin.this.global.type }] 139 | } 140 | } 141 | 142 | private[abide] object component extends { 143 | val global: AbidePlugin.this.global.type = AbidePlugin.this.global 144 | } with PluginComponent { 145 | val runsAfter = List("typer") 146 | val phaseName = AbidePlugin.this.name 147 | 148 | def newPhase(prev: Phase) = new StdPhase(prev) { 149 | override def name = AbidePlugin.this.name 150 | 151 | def apply(unit: CompilationUnit): Unit = { 152 | val warnings = analyzers.flatMap { 153 | gen => gen(_ => true)(unit.body) 154 | } 155 | 156 | presenters.foreach(_.apply(unit, warnings)) 157 | } 158 | } 159 | } 160 | 161 | private var abideCp: String = "" 162 | private var ruleClasses: List[String] = Nil 163 | private var analyzerClasses: List[String] = Nil 164 | private var presenterClasses: List[String] = Nil 165 | 166 | override def processOptions(options: List[String], error: String => Unit): Unit = { 167 | for (option <- options) { 168 | if (option.startsWith("ruleClass:")) { 169 | ruleClasses ::= option.substring("ruleClass:".length) 170 | } 171 | else if (option.startsWith("analyzerClass:")) { 172 | analyzerClasses ::= option.substring("analyzerClass:".length) 173 | } 174 | else if (option.startsWith("presenterClass:")) { 175 | presenterClasses ::= option.substring("presenterClass:".length) 176 | } 177 | else if (option.startsWith("abidecp:")) { 178 | abideCp = option.substring("abidecp:".length) 179 | } 180 | else { 181 | global.reporter.error(NoPosition, "Unexpected abide option: " + option) 182 | } 183 | } 184 | 185 | if (presenterClasses.isEmpty) { 186 | val defaultPresenter = ConsolePresenterGenerator 187 | val presenterPackage = defaultPresenter.getClass.getPackage.getName 188 | val presenterName = defaultPresenter.getClass.getName 189 | presenterClasses ::= s"$presenterPackage$presenterName" 190 | } 191 | } 192 | } 193 | --------------------------------------------------------------------------------