-
Notifications
You must be signed in to change notification settings - Fork 182
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
OrphanImplicits #635
Conversation
Can you please rebase this PR and cleanup the WIP commits? |
6cbfaaa
to
3521b22
Compare
There was a problem hiding this 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 |
There was a problem hiding this comment.
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]
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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 shapeF[G]
orF
that do not belong in the companion objects ofF
orG
There was a problem hiding this comment.
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 😄
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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) => |
There was a problem hiding this comment.
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/...
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
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. |
6efe39d
to
e4b2428
Compare
There was a problem hiding this 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.") |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. " + |
There was a problem hiding this comment.
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 thesymbol
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." |
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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) => |
There was a problem hiding this comment.
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), _) |
There was a problem hiding this comment.
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
scalafix/scalafix-core/shared/src/main/scala/scalafix/internal/rule/ExplicitResultTypes.scala
Lines 75 to 78 in e46a040
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)) => |
There was a problem hiding this comment.
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
}
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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?
ba920b7
to
cb0f471
Compare
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")) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 :)
There was a problem hiding this comment.
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>")) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) => |
There was a problem hiding this comment.
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)( |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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.
Superseded by #669 |
Depends on #634. Fixes #526.