Skip to content

Commit

Permalink
feat(RE-2): split into three subprojects
Browse files Browse the repository at this point in the history
* feat(RE-2): split into two artifacts

* chore: readme fix

* chore: consistency on docs formatting

* feat: trying to publish

* feat(RE-2): split into three projects and refactoring plugin internals

* feat(RE-2): make it work with nested case classes too

* feat(RE-2): refactoring

* feat(RE-2): refactoring

* feat(RE-2): adjustments in readme

* feat(RE-2): use TreeOps implicts methods

* feat(RE-2): further refinements & test

* feat(RE-2): bump sbt version

* feat(RE-2): minor fixes
  • Loading branch information
polentino authored Mar 20, 2024
1 parent c4bd5b3 commit 3bb142a
Show file tree
Hide file tree
Showing 20 changed files with 421 additions and 282 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@
metals.sbt
project/project/*
project/target/*
target/*
target/*
**/target/*
50 changes: 20 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,13 @@ it be better if you were simply to say "when I dump **the whole object**, I don'

## Usage

in your `project/plugins.sbt` file, add the following line (once it will be published :)
in your `build.sbt` file, add the following lines (once it will be published :)

```scala 3
addCompilerPlugin("io.github.polentino" % "redacted" % "0.0.1")
libraryDependencies ++= Seq(
"io.github.polentino" % "redacted" % redactedVersion,
compilerPlugin("io.github.polentino" % "redacted-plugin" % redactedVersion)
)
```

and then, in your case class definitions
Expand Down Expand Up @@ -111,10 +114,8 @@ will print
It also works with nested case classes:

```scala 3
case class Wrapper(id: String,
case class Wrapper(id: String, @redacted user: User)

@redacted: User
)
val wrapper = Wrapper("id-1", user) // user is the same object defined above
println(wrapper)
```
Expand All @@ -133,45 +134,34 @@ final case class User(id: UUID, @redacted name: String)
this compiler plugin will generate the following code

```scala 3
object User {
val __redactedFields = Seq("name")
}

final case class User(id: UUID, @redacted name: String) {
def toString(): String =
io.github.polentino.redacted.helpers.toRedactedString(this, User.__redactedFields)
def toString(): String = "User(" + this.id + ",***" + ")"
}
```

The way it's done is the following:

First, [EnrichCompanionObject](./src/main/scala/io/github/polentino/redacted/phases/EnrichCompanionObject.scala) phase
will be executed. This phase will check every companion object, be it automatically generated by the Scala compiler
itself or user-defined, find its corresponding companion case class and, if the case class has at least one field
annotated with `@redacted`, it will enrich the companion object with the `__redactedFields` field.
This field will be populated with the names of all fields annotated with `@redacted`.

After `EnrichCompanionObject` phase is
completed, [PatchToString](./src/main/scala/io/github/polentino/redacted/phases/PatchToString.scala) phase will then be
executed: for each case class that contains `@redacted` fields, this phase will:
1. get a reference to `__redactedFields` from its companion object,
2. patch the `toString` body to call the plugin specific implementation, and
3. pass the reference to `__redactedFields` as its second argument

The end result will be something like this:
[PatchToString](plugin/src/main/scala/io/github/polentino/redacted/phases/PatchToString.scala) phase will inspect every
class type definition and check whether the class being analysed is a `case class`, and if it has at least one of its
fields annotated with `@redacted` ; if that's the case, it will then proceed to rewrite the default `toString`
implementation by selectively returning either the `***` string, or the value of the field, depending on the presence
(or not) of that annotation.

```scala 3
def toString(): String =
io.github.polentino.redacted.helpers.toRedactedString(this, User.__redactedFields)
def toString(): String =
"<class name>(" + this.<field not redacted> + ",***" + ... + ")"
```

## Improvements

* [x] define the sequence of redacted fields in a private variable
* [x] move aforementioned variable in the case class companion object
* [ ] figure out why doesn't work anymore with nested case classes
* [ ] split into two artifacts (lib and compiler plugin)
* [ ] publish it in maven
* [x] figure out why doesn't work anymore with nested case classes
* [x] split into three artifacts (lib, compiler plugin and tests)
* [x] find a better alternative instead of using `managedSources`
* [x] refactor codebase
* [ ] create Sbt plugin
* [ ] publish library and compiler plugin to maven
* [ ] add some benchmarks with jmh

## Credits
Expand Down
89 changes: 76 additions & 13 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,20 +1,83 @@
ThisBuild / version := "0.2.0-SNAPSHOT"
ThisBuild / version := "0.3.0-SNAPSHOT"
ThisBuild / scalaVersion := "3.1.3"

ThisBuild / publishMavenStyle := true
ThisBuild / crossPaths := false
ThisBuild / versionScheme := Some("early-semver")
ThisBuild / publishTo := Some("GitHub Package Registry" at "https://maven.pkg.github.com/polentino/redacted")
ThisBuild / credentials += Credentials(
"GitHub Package Registry",
"maven.pkg.github.com",
"polentino",
sys.env.getOrElse("GITHUB_TOKEN", "???"))

inThisBuild(
List(
organization := "io.github.polentino",
homepage := Some(url("https://github.com/polentino/redacted")),
licenses := List(
"WTFPL" -> url("http://www.wtfpl.net/")
),
developers := List(
Developer(
"polentino",
"Diego Casella",
"polentino911@gmail.com",
url("https://be.linkedin.com/in/diegocasella")
)
)
)
)

Global / onChangedBuildSource := ReloadOnSourceChanges

lazy val root = (project in file("."))
.settings(
name := "redacted",
libraryDependencies ++= Seq(
"org.scala-lang" %% "scala3-compiler" % scalaVersion.value % "provided",
"org.scalatest" %% "scalatest" % "3.2.17" % "test"
),
scalafixDependencies += "com.liancheng" %% "organize-imports" % "0.6.0",
semanticdbEnabled := true,
semanticdbVersion := scalafixSemanticdb.revision,
Test / scalacOptions ++= {
val jar = (Compile / packageBin).value
Seq(s"-Xplugin:${jar.getAbsolutePath}", s"-Jdummy=${jar.lastModified}") // ensures recompile
name := "redacted-root",
publish / skip := true
)
.aggregate(redactedLibrary, redactedCompilerPlugin, redactedTests)

val scalafixSettings = Seq(
scalafixDependencies += "com.liancheng" %% "organize-imports" % "0.6.0",
semanticdbEnabled := true,
semanticdbVersion := scalafixSemanticdb.revision
)

lazy val redactedLibrary = (project in file("library"))
.settings(name := "redacted")
.settings(
scalafixSettings,
Test / skip := true
)

lazy val redactedCompilerPlugin = (project in file("plugin"))
.dependsOn(redactedLibrary)
.settings(name := "redacted-plugin")
.settings(scalafixSettings)
.settings(
Test / skip := true,
assembly / assemblyJarName := {
val assemblyJarFile = (Compile / Keys.`package`).value
assemblyJarFile.getName
},
libraryDependencies += "org.scala-lang" %% "scala3-compiler" % scalaVersion.value
)

lazy val redactedTests = (project in file("tests"))
.dependsOn(redactedLibrary)
.settings(name := "redacted-tests")
.settings(scalafixSettings)
.settings(
publish / skip := true,
libraryDependencies ++= Seq("org.scalatest" %% "scalatest" % "3.2.17" % Test),
scalacOptions ++= {
val jar = (redactedCompilerPlugin / assembly).value
val addPlugin = "-Xplugin:" + jar.getAbsolutePath
val dummy = "-Jdummy=" + jar.lastModified
Seq(addPlugin, dummy)
}
)

addCommandAlias("fmt", "all scalafmtSbt scalafmtAll test:scalafmtAll; scalafixAll")
addCommandAlias("fmt", "; scalafmtAll ; scalafmtSbt") // todo scalafix ?
addCommandAlias("fmtCheck", "; scalafmtCheckAll ; scalafmtSbtCheck")
File renamed without changes.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import dotty.tools.dotc.plugins.*
import io.github.polentino.redacted.phases._

class RedactedPlugin extends StandardPlugin {
override def init(options: List[String]): List[PluginPhase] = List(EnrichCompanionObject(), PatchToString())
override def init(options: List[String]): List[PluginPhase] = List(PatchToString())

override def name: String = "Redacted"

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.github.polentino.redacted.helpers

import dotty.tools.dotc.*
import dotty.tools.dotc.ast.tpd
import dotty.tools.dotc.core.Constants.Constant
import dotty.tools.dotc.core.Contexts.*
import dotty.tools.dotc.core.Symbols.*
import dotty.tools.dotc.core.{Flags, Symbols}

import io.github.polentino.redacted.redacted

object AstOps {
private val REDACTED_CLASS: String = classOf[redacted].getCanonicalName

def redactedSymbol(using Context): ClassSymbol = Symbols.requiredClass(REDACTED_CLASS)

extension (s: String)(using Context) {
def toConstantLiteral: tpd.Tree = tpd.Literal(Constant(s))
}

extension (symbol: Symbol)(using Context) {

def redactedFields: List[String] = {
val redactedType = redactedSymbol
symbol.primaryConstructor.paramSymss.flatten.collect {
case s if s.annotations.exists(_.matches(redactedType)) => s.name.toString
}
}
}

extension (tree: tpd.TypeDef)(using Context) {
def isCaseClass: Boolean = tree.symbol.is(Flags.CaseClass)
}
}
Loading

0 comments on commit 3bb142a

Please sign in to comment.