Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OrphanImplicits #635

Closed
wants to merge 18 commits into from
Closed

OrphanImplicits #635

wants to merge 18 commits into from

Conversation

vovapolu
Copy link
Contributor

Depends on #634. Fixes #526.

@fommil fommil changed the title OprhanImplicits OrphanImplicits Feb 25, 2018
@olafurpg
Copy link
Contributor

olafurpg commented Mar 6, 2018

Can you please rebase this PR and cleanup the WIP commits?

@vovapolu vovapolu force-pushed the implicit-rules branch 2 times, most recently from 6cbfaaa to 3521b22 Compare March 12, 2018 16:54
Copy link
Contributor

@olafurpg olafurpg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left a couple quick comments to clarify the expected functionality. Will need another round to review the implementation details

}

object Bar {
implicit val foo: Foo = ??? // error
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this an error? Foo is not of shape F[G]

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not a violation of

This rule bans definitions of implicits F[G] unless they are on the companion of the F or the G.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is a move to avoid implicits that are not of shape F[_] in scalaz, so I'd be happy with this as an error!

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(but it's not an orphan problem... so this should not error under this rule)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But is this an "orphan implicit error" or something else? I'll leave the final decision to you, just wanted to point out that the tests were not in sync with the documentation description.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not an orphan implicit

Copy link
Contributor Author

@vovapolu vovapolu Mar 16, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fommil what about F[A, B] form? Is it should be ignored or not?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

F[A, B] can look like F[_] if one of the types is fixed. If you can test if this implicit is defined on F, A or B that would be good.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this example up-to-date?

extends Rule("OrphanImplicits") {

override def description: String =
"Linter bans definitions of implicits F[G] unless they are on the companion of the F or the G"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Linter that reports an error on implicit instances of shape F[G] or F that do not belong in the companion objects of F or G

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is definitely about things of shape F[G], not about implicits of F on an F.

In fact I'd like a rule to ban implicits for anything except typeclasses 😄

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(ReaderT is a far superior alternative to implicit configuration objects)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The docs had an example of implicit val f: Foo = null so I assumed that was intended behavior too. Drop the "or F" part then

}
}
new ContextTraverser[LintMessage, List[Symbol.Global]](List.empty)({
case (Defn.Object(_, name, _), objs) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what about implicit instances outside of objects? For example traits/classes/term.block/...

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the trait rule be fairly complex to implement. It could be enough to say that it's only allowed on a trait if it's in the same compilation unit as the F or G. To combat implicit ordering, you end up with crazy scaffolding like this https://github.com/scalaz/scalaz/blob/series/7.3.x/core/src/main/scala/scalaz/EitherT.scala#L297-L373

new ContextTraverser[LintMessage, List[Symbol.Global]](List.empty)({
case (Defn.Object(_, name, _), objs) =>
index.symbol(name) match {
case Some(s @ Symbol.Global(_, _)) => Right(s :: objs)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this report an error?

trait Foo[T]
object Foo {
  object Bar {
    implicit val x: Foo[Int] = null
  }
}

It should be an error IMO because the implicit is not a member of the Foo companion object.

@fommil
Copy link
Contributor

fommil commented Mar 16, 2018

in hindsight, I think this is very much related to #661 ... 661 is about what is allowed to be implicit and this is about where the implicits can be defined.

Copy link
Contributor

@olafurpg olafurpg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Left some comments on both documentation and implementation.

import metaconfig.generic.Surface

case class DisabledSymbol(
@Description("Symbol to ban.")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A single symbol to ban. Cannot be used together with the regex option.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we give examples here?

  • scala.Predef.any2stringadd
  • scala.sys.process.Process

@Description("Custom id for error messages.")
id: Option[String],
@Description(
"Regex to ban instead of one symbol. " +
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Regex to ban symbols matching a given include/exclude patterns.
Cannot be used together with the symbol option.
Include and exclude filters can be either a list of regex strings or a single regex string.


override def description: String =
"Linter that reports an error on implicit instances of shape F[G] " +
"that do not belong in the companion objects of F or G."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that do not belong to the companion object

"that do not belong in the companion objects of F or G."

private lazy val errorCategory: LintCategory =
LintCategory.error("Orphan implicits should be avoided")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this description can be "" because you provide a more detailed message below.

errorCategory
.at(
s"""Orphan implicits are not allowed.
|You should put this definition to one of the following objects:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This definition is only allowed in one of the following objects:

}

ctx.tree.collect {
case Defn.Object(_, name, templ) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

case Defn.Object(_, name @ index.Symbol(obj), templ) =>

index.symbol(name) match {
case Some(obj @ Symbol.Global(_, _)) =>
templ.stats.collect {
case t @ Defn.Val(mods, _, Some(tpe), _)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't catch implicits with inferred types

object Foo {
  implicit val x = List(1)
}

The robust way to implement this is to query the type of a definition is to use the .resultType extension method as is done here

for {
name <- defnName(defn)
symbol <- name.symbol
typ <- symbol.resultType

.resultTypes will soon be refactored to use Denotation.tpe: semanticdb3.Type, but I can take care of that refactoring once that happens.

pos: Position): Option[LintMessage] = {

val tpeSymbols = tpe match {
case Type.Apply(t, Seq(arg)) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This won't handle cases with multiple type arguments

object Foo {
  implicit val x: Map[String, Int] = Map.empty
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it won't :) As I understand @fommil wants only F[G] form or some complicated detection of fixed type-vars.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in hindsight, #661 is a prerequisite for doing orphan implicits properly... it bans everything except typeclasses. And then with what's left we can seach on all the type parameters to see if it's defined in a good place or not.

}

object Bar {
implicit val foo: Foo = ??? // error
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this example up-to-date?

@olafurpg
Copy link
Contributor

in hindsight, #661 is a prerequisite for doing orphan implicits properly... it bans everything except typeclasses. And then with what's left we can seach on all the type parameters to see if it's defined in a good place or not.

Should the two features be under the same rule? If yes, what should it be named and what are the configuration options?

symbolGlobalReader
.read(s)
.map(sym => DisabledSymbol(Some(sym), None, None, None))
case _ => Configured.NotOk(ConfError.message("Wrong config format"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

.instance allows you to pass a partial function and metaconfig produces a slightly more helpful error message

obj: Symbol.Global): Option[LintMessage] = {
if (mods.exists(_.is[Mod.Implicit])) {
tpe match {
case Some(t) => handleImplicit(t, obj, d.pos)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would report false positives on situations like

trait Writer[T]
object Writer {
  type IntWriter = Writer[Int]
  implicit val x: IntWriter = ...
}

I would not rely on the syntactic structure of the type in the source.

Copy link
Contributor Author

@vovapolu vovapolu Mar 26, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well, symbol.resultType doesn't work in this case too :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh, you're right! The new semanticdb Type does handle that case however, you can leave it unchanged for now 😉

import scala.collection.mutable // ok

@SuppressWarnings(Array("Disable.ListBuffer"))
@SuppressWarnings(Array("Disable.<init>"))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should not report duplicate errors on new X if X is disabled. I think it would be better to skip <init> because there is no way to target a constructor without referencing the explicit type.

Not sure what to do if the user wants to disable a specific constructor overload.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case I'm disabling regex "scala.collection.mutable.*". I'm not sure there's a good solution to ignore <init> in regexes. I'd say regex initially is kind of hack, everyone who uses it should expect it's not a silver bullet.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a good usecase is to allow creating File, but not doing anything with it. I'm fine with this meaning that overloads are treated equally. I custom syntax in the future might be nice, but it's a second order feature for sure.

}

ctx.tree.collect {
case Defn.Object(_, name @ index.Symbol(sym), templ) =>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

case Defn.Object(_, name @ index.Symbol(sym: Symbol.Global), templ) => 

* At every tree node, either builds a new Context or returns a new Value to accumulate.
* To collect all accumulated values, use result(Tree).
*/
class ContextTraverser[Value, Context](initContext: Context)(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this still used by OrphanImplicits?

Left(
createLintMessage(s, disabled, t.pos)
)
SymbolOps.normalize(s) match {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it better than the old version? It doesn't affect tests. I added this because of your suggestion to disable normalized symbols.

@olafurpg
Copy link
Contributor

olafurpg commented Apr 4, 2018

Superseded by #669

@olafurpg olafurpg closed this Apr 4, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

ban orphan implicits
3 participants