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

Add LoggerFactory typeclass #629

Merged
merged 15 commits into from
Apr 21, 2022
8 changes: 7 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import com.typesafe.tools.mima.core._

val Scala213 = "2.13.8"
val Scala212 = "2.12.15"
val Scala3 = "3.0.2"
Expand Down Expand Up @@ -45,7 +47,11 @@ lazy val core = crossProject(JSPlatform, JVMPlatform)
name := "log4cats-core",
libraryDependencies ++= Seq(
"org.typelevel" %%% "cats-core" % catsV
)
),
libraryDependencies ++= {
if (tlIsScala3.value) Seq.empty
else Seq("org.scala-lang" % "scala-reflect" % scalaVersion.value % Provided)
}
)

lazy val testing = crossProject(JSPlatform, JVMPlatform)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
/*
* Copyright 2018 Typelevel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.typelevel.log4cats

import org.typelevel.log4cats.internal.LoggerNameMacro

trait LoggerNameCompat {
implicit def name: LoggerName = macro LoggerNameMacro.getLoggerName
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* Copyright 2018 Typelevel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.typelevel.log4cats.internal

import scala.annotation.tailrec
import scala.reflect.macros.blackbox

private[log4cats] class LoggerNameMacro(val c: blackbox.Context) {
final def getLoggerName = SharedLoggerNameMacro.getLoggerNameImpl(c)
}

private[log4cats] object SharedLoggerNameMacro {

/** Get a logger by reflecting the enclosing class name. */
private[log4cats] def getLoggerNameImpl(c: blackbox.Context) = {
import c.universe._

@tailrec def findEnclosingClass(sym: c.universe.Symbol): c.universe.Symbol = {
sym match {
case NoSymbol =>
c.abort(c.enclosingPosition, s"Couldn't find an enclosing class or module for the logger")
case s if s.isModule || s.isClass =>
s
case other =>
/* We're not in a module or a class, so we're probably inside a member definition. Recurse upward. */
findEnclosingClass(other.owner)
}
}

val cls = findEnclosingClass(c.internal.enclosingOwner)

assert(cls.isModule || cls.isClass, "Enclosing class is always either a module or a class")

def nameBySymbol(s: Symbol) = {
def fullName(s: Symbol): String = {
@inline def isPackageObject = (
(s.isModule || s.isModuleClass)
&& s.owner.isPackage
&& s.name.decodedName.toString == termNames.PACKAGE.decodedName.toString
)

if (s.isModule || s.isClass) {
if (isPackageObject) {
s.owner.fullName
} else if (s.owner.isStatic) {
s.fullName
} else {
fullName(s.owner) + "." + s.name.encodedName.toString
}
} else {
fullName(s.owner)
}
}

q"new _root_.org.typelevel.log4cats.LoggerName(${fullName(s)})"
}

def nameByType(s: Symbol) = {
val typeSymbol: ClassSymbol = (if (s.isModule) s.asModule.moduleClass else s).asClass
val typeParams = typeSymbol.typeParams

if (typeParams.isEmpty) {
q"new _root_.org.typelevel.log4cats.LoggerName(_root_.scala.Predef.classOf[$typeSymbol].getName)"
} else {
if (typeParams.exists(_.asType.typeParams.nonEmpty)) {
/* We have at least one higher-kinded type: fall back to by-name logger construction, as
* there's no simple way to declare a higher-kinded type with an "any" parameter. */
nameBySymbol(s)
} else {
val typeArgs = List.fill(typeParams.length)(WildcardType)
val typeConstructor = tq"$typeSymbol[..${typeArgs}]"
q"new _root_.org.typelevel.log4cats.LoggerName(_root_.scala.Predef.classOf[$typeConstructor].getName)"
}
}
}

@inline def isInnerClass(s: Symbol) =
s.isClass && !(s.owner.isPackage)

val instanceByName =
(true && cls.isModule || cls.isModuleClass) || (cls.isClass && isInnerClass(
cls
))

if (instanceByName) {
nameBySymbol(cls)
} else {
nameByType(cls)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright 2018 Typelevel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.typelevel.log4cats

import org.typelevel.log4cats.internal.LoggerNameMacro

import scala.quoted.*

trait LoggerNameCompat {
implicit inline def name: LoggerName =
${ LoggerNameMacro.getLoggerName }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright 2018 Typelevel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.typelevel.log4cats
package internal

import scala.annotation.tailrec
import scala.quoted.*

private[log4cats] object LoggerNameMacro {
def getLoggerName(using qctx: Quotes): Expr[LoggerName] = {
val name = getLoggerNameImpl
'{ new LoggerName($name) }
}

def getLoggerNameImpl(using qctx: Quotes): Expr[String] = {
import qctx.reflect._

@tailrec def findEnclosingClass(sym: Symbol): Symbol = {
sym match {
case s if s.isNoSymbol =>
report.throwError("Couldn't find an enclosing class or module for the logger")
case s if s.isClassDef =>
s
case other =>
/* We're not in a module or a class, so we're probably inside a member definition. Recurse upward. */
findEnclosingClass(other.owner)
}
}

def symbolName(s: Symbol): Expr[String] = {
def fullName(s: Symbol): String = {
val flags = s.flags
if (flags.is(Flags.Package)) {
s.fullName
} else if (s.isClassDef) {
if (flags.is(Flags.Module)) {
if (s.name == "package$") {
fullName(s.owner)
} else {
val chomped = s.name.stripSuffix("$")
fullName(s.owner) + "." + chomped
}
} else {
fullName(s.owner) + "." + s.name
}
} else {
fullName(s.owner)
}
}

Expr(fullName(s).stripSuffix("$"))
}

val cls = findEnclosingClass(Symbol.spliceOwner)
symbolName(cls)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/*
* Copyright 2018 Typelevel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.typelevel.log4cats

import scala.annotation.implicitNotFound

@implicitNotFound("""
Implicit not found for LoggerFactory[${F}].
Information can be found here: https://log4cats.github.io/logging-capability.html
""")
trait LoggerFactory[F[_]] extends LoggerFactoryGen[F] {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

we can consider dropping this, since LoggerFactoryGen now has everything that is needed.

Choose a reason for hiding this comment

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

It's kinda convinient for users who would like to use just LoggerFactory without the need to define LoggerType explicitly

type LoggerType = SelfAwareStructuredLogger[F]
}

object LoggerFactory extends LoggerFactoryGenCompanion {
def apply[F[_]: LoggerFactory]: LoggerFactory[F] = implicitly
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright 2018 Typelevel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.typelevel.log4cats

trait LoggerFactoryGen[F[_]] {
type LoggerType <: Logger[F]
def getLogger(implicit name: LoggerName): LoggerType = getLoggerFromName(name.value)
def getLoggerFromClass(clazz: Class[_]): LoggerType = getLoggerFromName(clazz.getName)
def create(implicit name: LoggerName): F[LoggerType] = fromName(name.value)
def fromClass(clazz: Class[_]): F[LoggerType] = fromName(clazz.getName)
def getLoggerFromName(name: String): LoggerType
def fromName(name: String): F[LoggerType]
}

private[log4cats] trait LoggerFactoryGenCompanion {
def getLogger[F[_]](implicit lf: LoggerFactoryGen[F], name: LoggerName): lf.LoggerType =
lf.getLogger
def getLoggerFromName[F[_]](name: String)(implicit lf: LoggerFactoryGen[F]): lf.LoggerType =
lf.getLoggerFromName(name)
def getLoggerFromClass[F[_]](clazz: Class[_])(implicit lf: LoggerFactoryGen[F]): lf.LoggerType =
lf.getLoggerFromClass(clazz)
def create[F[_]](implicit lf: LoggerFactoryGen[F], name: LoggerName): F[lf.LoggerType] =
lf.create
def fromName[F[_]](name: String)(implicit lf: LoggerFactoryGen[F]): F[lf.LoggerType] =
lf.fromName(name)
def fromClass[F[_]](clazz: Class[_])(implicit lf: LoggerFactoryGen[F]): F[lf.LoggerType] =
lf.fromClass(clazz)
}
21 changes: 21 additions & 0 deletions core/shared/src/main/scala/org/typelevel/log4cats/LoggerName.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/*
* Copyright 2018 Typelevel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.typelevel.log4cats

object LoggerName extends LoggerNameCompat

final case class LoggerName(value: String) extends AnyVal
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2018 Typelevel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.typelevel.log4cats

import munit.FunSuite

class LoggerNameTest extends FunSuite {
class FooA {
val name = LoggerName.name
}

object FooB {
val name = LoggerName.name
}

test("names") {
val name1 = new FooA().name
val name2 = FooB.name

assertEquals(name1.value, "org.typelevel.log4cats.LoggerNameTest.FooA")
assertEquals(name2.value, "org.typelevel.log4cats.LoggerNameTest.FooB")
}
}
Loading