├── .github └── workflows │ └── build.yml ├── .scalafmt.conf ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.sbt ├── input └── src │ └── main │ └── scala │ └── fix │ ├── Affine.scala │ ├── Box.scala │ ├── Closeable.scala │ ├── Closer.scala │ ├── Overused.scala │ ├── SingleUse.scala │ └── Unused.scala ├── library └── src │ └── main │ └── java │ └── com │ └── earldouglas │ └── linearscala │ └── Linear.java ├── plugin ├── build.sbt ├── project │ └── plugins.sbt └── src │ ├── main │ └── scala │ │ └── LinearScala.scala │ └── sbt-test │ └── linear-scala │ └── simple │ ├── project │ └── plugins.sbt │ ├── src │ └── main │ │ └── examples │ │ ├── FieldUsedTwice.scala │ │ ├── Linear.scala │ │ ├── UnusedField.scala │ │ └── UnusedParameter.scala │ └── test ├── project ├── build.properties └── plugins.sbt ├── rules └── src │ └── main │ ├── resources │ └── META-INF │ │ └── services │ │ └── scalafix.v1.Rule │ └── scala │ └── fix │ └── LinearTypes.scala └── tests └── src └── test └── scala └── fix └── RuleSuite.scala /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-24.04 9 | 10 | steps: 11 | 12 | - uses: actions/checkout@v4 13 | 14 | - uses: actions/setup-java@v4 15 | with: 16 | java-version: 8 17 | distribution: temurin 18 | cache: sbt 19 | 20 | - uses: sbt/setup-sbt@v1 21 | 22 | - run: sbt scalafmtCheckAll 23 | - run: sbt tests/test 24 | - run: sbt 'set ThisBuild / version := "0.1.0-SNAPSHOT"; library/publishLocal; +rules/publishLocal; plugin/scripted' 25 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = 3.9.7 // https://scalameta.org/scalafmt/docs/installation.html#sbt 2 | runner.dialect = scala213 // https://scalameta.org/scalafmt/docs/configuration.html#scala-dialects 3 | maxColumn = 72 // RFC 678: https://datatracker.ietf.org/doc/html/rfc678 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | [![Build Status][build-badge]][build-link] 2 | [![Release Artifacts][release-badge]][release-link] 3 | 4 | [build-badge]: https://github.com/earldouglas/linear-scala/workflows/build/badge.svg "Build Status" 5 | [build-link]: https://github.com/earldouglas/linear-scala/actions "GitHub Actions" 6 | [release-link]: https://oss.sonatype.org/content/repositories/releases/com/earldouglas/linear-scala/ "Sonatype Releases" 7 | [release-badge]: https://img.shields.io/nexus/r/https/oss.sonatype.org/com.earldouglas/linear-scala "Sonatype Releases" 8 | 9 | # Contributing 10 | 11 | ## Testing 12 | 13 | Using scalafix-testkit: 14 | 15 | ``` 16 | $ sbt 17 | > tests/test 18 | [info] All tests passed. 19 | ``` 20 | 21 | ## Publishing 22 | 23 | Two modules are published from this project: 24 | 25 | * *linear-scala*: a standard dependency providing the `Linear` type 26 | * *linear-scala-scalafix*: a Scalafix dependency providing the 27 | `LinearTypes` rule 28 | 29 | ``` 30 | $ export VERSION=0.0.1 31 | $ sbt 32 | > set ThisBuild / version := sys.env("VERSION") 33 | > library/publishSigned 34 | > +rules/publishSigned 35 | > plugin/publishSigned 36 | > sonatypeBundleRelease 37 | $ git tag $VERSION 38 | $ git push --tags 39 | ``` 40 | 41 | ## References 42 | 43 | ### Scalafix 44 | 45 | * https://scalacenter.github.io/scalafix/docs/developers/tutorial.html 46 | * https://www.javadoc.io/doc/ch.epfl.scala/scalafix-core_2.13/latest/scalafix/index.html 47 | 48 | ### Scalameta 49 | 50 | * https://scalameta.org/docs/trees/guide.html 51 | * https://scalameta.org/docs/semanticdb/guide.html 52 | * https://www.javadoc.io/doc/org.scalameta/trees_2.13/latest/scala/meta/index.html 53 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 James Earl Douglas 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted, provided that the above 5 | copyright notice and this permission notice appear in all copies. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 8 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 9 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 10 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 11 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 12 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 13 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status][build-badge]][build-link] 2 | [![Release Artifacts][release-badge]][release-link] 3 | 4 | [build-badge]: https://github.com/earldouglas/linear-scala/workflows/build/badge.svg "Build Status" 5 | [build-link]: https://github.com/earldouglas/linear-scala/actions "GitHub Actions" 6 | [release-link]: https://oss.sonatype.org/content/repositories/releases/com/earldouglas/linear-scala/ "Sonatype Releases" 7 | [release-badge]: https://img.shields.io/nexus/r/https/oss.sonatype.org/com.earldouglas/linear-scala "Sonatype Releases" 8 | 9 | # linear-scala 10 | 11 | linear-scala adds support for linear types in Scala via a custom 12 | Scalafix linter. 13 | 14 | ## Setup 15 | 16 | *project/plugins.sbt:* 17 | 18 | ```scala 19 | addSbtPlugin("com.earldouglas" % "sbt-linear-scala" % "0.0.3") 20 | ``` 21 | 22 | ## Usage 23 | 24 | Mix in the `Linear` interface to prevent values from being 25 | under/over-used. 26 | 27 | ```scala 28 | import com.earldouglas.linearscala.Linear 29 | 30 | case class Box(value: Int) extends Linear 31 | ``` 32 | 33 | Scalafix finds values that are never used: 34 | 35 | ```scala 36 | trait UnusedField { 37 | val box: Box = Box(42) // error: box is never used 38 | } 39 | 40 | trait UnusedParameter { 41 | def foo(x: Box, y: Box): Int = // error: y is never used 42 | x.value 43 | } 44 | ``` 45 | 46 | Scalafix also finds values that are used multiple times: 47 | 48 | ```scala 49 | trait FieldUsedTwice { 50 | val box: Box = Box(42) 51 | println(box) // error: box is used twice 52 | println(box) // error: box is used twice 53 | } 54 | ``` 55 | 56 | See the tests in [input/src/main/scala/fix/](input/src/main/scala/fix/) 57 | for more examples. 58 | 59 | ## References 60 | 61 | ### Linear Types 62 | 63 | * 64 | * 65 | * 66 | * 67 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | lazy val V = _root_.scalafix.sbt.BuildInfo 2 | 3 | inThisBuild( 4 | List( 5 | scalaVersion := V.scala213, 6 | crossScalaVersions := List(V.scala213, V.scala212), 7 | organization := "com.earldouglas", 8 | homepage := Some( 9 | url("https://github.com/earldouglas/linear-scala") 10 | ), 11 | licenses := List( 12 | ("ISC", url("https://opensource.org/licenses/ISC")) 13 | ), 14 | developers := List( 15 | Developer( 16 | "earldouglas", 17 | "James Earl Douglas", 18 | "james@earldouglas.com", 19 | url("https://earldouglas.com") 20 | ) 21 | ), 22 | addCompilerPlugin(scalafixSemanticdb), 23 | scalacOptions ++= 24 | List("-Yrangepos", "-P:semanticdb:synthetics:on"), 25 | versionScheme := Some("semver-spec") 26 | ) 27 | ) 28 | 29 | publish / skip := true 30 | 31 | lazy val library = // a standard dependency providing the `Linear` type 32 | project 33 | .settings( 34 | moduleName := "linear-scala", 35 | crossPaths := false // publish without the Scala version postfix in artifact names 36 | ) 37 | 38 | lazy val rules = // a Scalafix dependency providing the `LinearTypes` rule 39 | project 40 | .settings( 41 | moduleName := "linear-scala-scalafix", 42 | libraryDependencies += "ch.epfl.scala" %% "scalafix-core" % V.scalafixVersion 43 | ) 44 | 45 | lazy val input = // sample input to use for testing; annotated with expected errors 46 | project 47 | .settings(publish / skip := true) 48 | .dependsOn(rules) 49 | .dependsOn(library) 50 | 51 | lazy val output = // empty since we're building a linter and not a rewriter, but needed by scalafix-testkit 52 | project 53 | .settings(publish / skip := true) 54 | 55 | lazy val tests = // boilerplate to be able to use scalafix-testkit 56 | project 57 | .settings( 58 | publish / skip := true, 59 | libraryDependencies += "ch.epfl.scala" % "scalafix-testkit" % V.scalafixVersion % Test cross CrossVersion.full, 60 | Compile / compile := (Compile / compile) 61 | .dependsOn(input / Compile / compile) 62 | .value, 63 | scalafixTestkitOutputSourceDirectories := (output / Compile / unmanagedSourceDirectories).value, 64 | scalafixTestkitInputSourceDirectories := (input / Compile / unmanagedSourceDirectories).value, 65 | scalafixTestkitInputClasspath := (input / Compile / fullClasspath).value 66 | ) 67 | .dependsOn(rules) 68 | .dependsOn(library) 69 | .enablePlugins(ScalafixTestkitPlugin) 70 | 71 | lazy val plugin = 72 | project 73 | .in(file("plugin")) 74 | .settings(moduleName := "sbt-linear-scala") 75 | -------------------------------------------------------------------------------- /input/src/main/scala/fix/Affine.scala: -------------------------------------------------------------------------------- 1 | /* 2 | rule = LinearTypes 3 | */ 4 | package fix 5 | 6 | import com.earldouglas.linearscala.Linear 7 | import java.io.RandomAccessFile 8 | 9 | class Affine[E <: Linear, A](val run: E => A) { 10 | 11 | def flatMap[B](f: A => Affine[E, B]): Affine[E, B] = 12 | new Affine({ e: E => 13 | val a: A = run(e) 14 | f(a).run(e) 15 | }) 16 | 17 | def map[B](f: A => B): Affine[E, B] = 18 | flatMap { a: A => 19 | new Affine(_ => f(a)) 20 | } 21 | } 22 | 23 | class AffineConstructor[E] { 24 | def apply[A](run: E => A): Affine[E with Linear, A] = 25 | new Affine(run.asInstanceOf[E with Linear => A]) 26 | } 27 | 28 | object Affine { 29 | def apply[E]: AffineConstructor[E] = 30 | new AffineConstructor 31 | } 32 | 33 | /** Don't allow a [[Linear]] file to be dereferenced more than once, 34 | * except for where we need to. 35 | */ 36 | trait ReadFromLinearWithAffine { 37 | 38 | import java.nio.charset.StandardCharsets 39 | 40 | val readAndCloseFile: Affine[RandomAccessFile with Linear, String] = 41 | for { 42 | length <- Affine[RandomAccessFile] { f => f.length() } 43 | content <- Affine[RandomAccessFile] { f => 44 | val buf: Array[Byte] = new Array(length.toInt) 45 | f.readFully(buf) 46 | new String(buf, StandardCharsets.UTF_8) 47 | } 48 | _ <- Affine[RandomAccessFile] { f => f.close() } 49 | } yield content 50 | 51 | val f: RandomAccessFile with Linear = 52 | new RandomAccessFile("/etc/passwd", "r") 53 | .asInstanceOf[RandomAccessFile with Linear] 54 | 55 | readAndCloseFile.run(f) // assert: LinearTypes 56 | f.readLine() // assert: LinearTypes 57 | } 58 | -------------------------------------------------------------------------------- /input/src/main/scala/fix/Box.scala: -------------------------------------------------------------------------------- 1 | /* 2 | rule = LinearTypes 3 | */ 4 | package fix 5 | 6 | import com.earldouglas.linearscala.Linear 7 | 8 | /** Mix in the [[Linear]] interface to mark instances for use exactly 9 | * once. 10 | */ 11 | case class Box(value: Int) extends Linear 12 | -------------------------------------------------------------------------------- /input/src/main/scala/fix/Closeable.scala: -------------------------------------------------------------------------------- 1 | /* 2 | rule = LinearTypes 3 | */ 4 | package fix 5 | 6 | import com.earldouglas.linearscala.Linear 7 | import java.io.RandomAccessFile 8 | 9 | /** Don't allow a [[Linear]] file to be dereferenced more than once, 10 | * because it might already be closed. 11 | */ 12 | trait ReadFromClosedCloseable { 13 | 14 | def readLineAndCloseFile( 15 | f: RandomAccessFile 16 | ): String = { 17 | val line = f.readLine() 18 | f.close() 19 | line 20 | } 21 | 22 | val f: RandomAccessFile with Linear = 23 | new RandomAccessFile("/etc/passwd", "r") 24 | .asInstanceOf[RandomAccessFile with Linear] 25 | 26 | readLineAndCloseFile(f) // assert: LinearTypes 27 | f.readLine() // assert: LinearTypes 28 | } 29 | -------------------------------------------------------------------------------- /input/src/main/scala/fix/Closer.scala: -------------------------------------------------------------------------------- 1 | /* 2 | rule = LinearTypes 3 | */ 4 | package fix 5 | 6 | import com.earldouglas.linearscala.Linear 7 | import java.io.Closeable 8 | import java.io.RandomAccessFile 9 | 10 | case class Closer[C <: Closeable, A](unsafeRun: C => A) { 11 | 12 | def run(c: C): A = { 13 | val a = unsafeRun(c) 14 | println("***CLOSING***") 15 | c.close() 16 | a 17 | } 18 | 19 | def flatMap[B](f: A => Closer[C, B]): Closer[C, B] = 20 | Closer { c: C => 21 | val a: A = unsafeRun(c) 22 | f(a).unsafeRun(c) 23 | } 24 | 25 | def map[B](f: A => B): Closer[C, B] = 26 | flatMap { a: A => 27 | Closer(_ => f(a)) 28 | } 29 | } 30 | 31 | /** Don't allow a [[Linear]] file to be dereferenced more than once, 32 | * because it might already be closed. 33 | */ 34 | trait ReadFromClosedCloseableWithCloser { 35 | 36 | import java.nio.charset.StandardCharsets 37 | 38 | val readAndCloseFile: Closer[RandomAccessFile, String] = 39 | for { 40 | length <- Closer { f: RandomAccessFile => f.length() } 41 | content <- Closer { f: RandomAccessFile => 42 | val buf: Array[Byte] = new Array(length.toInt) 43 | f.readFully(buf) 44 | new String(buf, StandardCharsets.UTF_8) 45 | } 46 | } yield content 47 | 48 | val f: RandomAccessFile with Linear = 49 | new RandomAccessFile("/etc/passwd", "r") 50 | .asInstanceOf[RandomAccessFile with Linear] 51 | 52 | readAndCloseFile.run(f) // assert: LinearTypes 53 | f.readLine() // assert: LinearTypes 54 | } 55 | -------------------------------------------------------------------------------- /input/src/main/scala/fix/Overused.scala: -------------------------------------------------------------------------------- 1 | /* 2 | rule = LinearTypes 3 | */ 4 | package fix 5 | 6 | import com.earldouglas.linearscala.Linear 7 | 8 | /** Don't allow a [[Box]] field to be dereferenced more than once. 9 | */ 10 | trait FieldUsedTwice { 11 | val box: Box = Box(42) 12 | println(box) // assert: LinearTypes 13 | println(box) // assert: LinearTypes 14 | } 15 | 16 | /** Don't allow a [[Box]] binding in a for comprehension to be 17 | * dereferenced more than once. 18 | */ 19 | trait BindingUsedTwice { 20 | for { 21 | x <- Some(Box(6)) 22 | y <- Some(Box(x.value + 1)) // assert: LinearTypes 23 | z <- Some(Box(x.value * y.value)) // assert: LinearTypes 24 | } yield z 25 | 26 | for { 27 | x <- Some(Box(6)) 28 | y <- Some(Box(7)) 29 | z <- Some(Box(x.value * y.value)) // assert: LinearTypes 30 | } yield (x, y, z) // assert: LinearTypes 31 | } 32 | 33 | /** Don't allow a field with a [[Linear]] structural type to be 34 | * dereferenced more than once. 35 | */ 36 | trait FieldWithStructuralTypeUsedTwice { 37 | val x: Int with Linear = 42.asInstanceOf[Int with Linear] 38 | println(x) // assert: LinearTypes 39 | println(x) // assert: LinearTypes 40 | } 41 | -------------------------------------------------------------------------------- /input/src/main/scala/fix/SingleUse.scala: -------------------------------------------------------------------------------- 1 | /* 2 | rule = LinearTypes 3 | */ 4 | package fix 5 | 6 | /** Allow a [[Box]] field to be dereferenced exactly once. 7 | */ 8 | trait FieldUsedOnce { 9 | val box: Box = Box(42) 10 | println(box) 11 | } 12 | -------------------------------------------------------------------------------- /input/src/main/scala/fix/Unused.scala: -------------------------------------------------------------------------------- 1 | /* 2 | rule = LinearTypes 3 | */ 4 | package fix 5 | 6 | import com.earldouglas.linearscala.Linear 7 | 8 | /** Don't allow a [[Box]] field to be created but never dereferenced. 9 | */ 10 | trait UnusedField { 11 | val box: Box = // assert: LinearTypes 12 | Box(42) 13 | } 14 | 15 | /** Don't allow a [[Box]] parameter to be declared but never 16 | * dereferenced. 17 | */ 18 | trait UnusedParameter { 19 | def foo( 20 | x: Box, 21 | y: Box // assert: LinearTypes 22 | ): Int = 23 | x.value 24 | } 25 | 26 | /** Don't allow a [[Box]] method to be created but never called. 27 | */ 28 | trait UnusedMethod { 29 | def foo(): Box = // assert: LinearTypes 30 | Box(42) 31 | } 32 | 33 | /** Don't allow a [[Box]] value to be created but never dereferenced. 34 | */ 35 | trait UnusedValue { 36 | def foo(): Unit = { 37 | val x: Box = Box(42) // assert: LinearTypes 38 | } 39 | } 40 | 41 | /** Don't allow a [[Box]] binding in a for comprehension to be created 42 | * but never dereferenced. 43 | */ 44 | trait UnusedBinding { 45 | for { 46 | x <- Some(Box(6)) // assert: LinearTypes 47 | y <- Some(Box(7)) // assert: LinearTypes 48 | z <- Some(Box(42)) 49 | } yield z 50 | } 51 | 52 | /** Don't allow a field with a [[Linear]] structural type to be created 53 | * but never dereferenced. 54 | */ 55 | trait UnusedFieldWithStructuralType { 56 | val x: Int with Linear = // assert: LinearTypes 57 | 42.asInstanceOf[Int with Linear] 58 | } 59 | 60 | /** Don't allow a parameter with a [[Linear]] structural type to be 61 | * declared but never dereferenced. 62 | */ 63 | trait UnusedParameterWithStructuralType { 64 | def foo( 65 | x: Int, 66 | y: Int with Linear // assert: LinearTypes 67 | ): Int = 68 | 42 69 | } 70 | 71 | /** Don't allow a [[Box]] binding in a for comprehension to be shadowed 72 | * but never dereferenced. 73 | */ 74 | trait UnusedShadow { 75 | for { 76 | x <- Some(Box(6)) // assert: LinearTypes 77 | y <- Some(Box(7)) 78 | x <- Some(Box(8)) 79 | z <- Some(Box(x.value * y.value)) 80 | } yield z 81 | } 82 | -------------------------------------------------------------------------------- /library/src/main/java/com/earldouglas/linearscala/Linear.java: -------------------------------------------------------------------------------- 1 | package com.earldouglas.linearscala; 2 | 3 | public interface Linear { } 4 | -------------------------------------------------------------------------------- /plugin/build.sbt: -------------------------------------------------------------------------------- 1 | scalaVersion := _root_.scalafix.sbt.BuildInfo.scala212 2 | scalacOptions ++= 3 | Seq( 4 | "-deprecation", 5 | "-encoding", 6 | "utf8", 7 | "-feature", 8 | "-language:existentials", 9 | "-language:experimental.macros", 10 | "-language:higherKinds", 11 | "-language:implicitConversions", 12 | "-unchecked", 13 | "-Xfatal-warnings", 14 | "-Xlint", 15 | "-Ypartial-unification", 16 | "-Yrangepos", 17 | "-Ywarn-unused", 18 | "-Ywarn-unused-import" 19 | ) 20 | 21 | sbtPlugin := true 22 | enablePlugins(SbtPlugin) 23 | addSbtPlugin( 24 | "ch.epfl.scala" % "sbt-scalafix" % _root_.scalafix.sbt.BuildInfo.scalafixVersion 25 | ) 26 | 27 | scriptedBufferLog := false 28 | scriptedLaunchOpts += "-Dplugin.version=" + version.value 29 | 30 | Compile / sourceGenerators += task { 31 | val dir = (Compile / sourceManaged).value 32 | val className = "LinearScalaBuildInfo" 33 | val f = dir / s"${className}.scala" 34 | IO.write( 35 | f, 36 | Seq( 37 | "package com.earldouglas.linearscala", 38 | "", 39 | s"object $className {", 40 | s""" def version: String = "${version.value}" """, 41 | "}" 42 | ).mkString("", "\n", "\n") 43 | ) 44 | Seq(f) 45 | } 46 | -------------------------------------------------------------------------------- /plugin/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | libraryDependencies += "org.scala-sbt" %% "scripted-plugin" % sbtVersion.value 2 | -------------------------------------------------------------------------------- /plugin/src/main/scala/LinearScala.scala: -------------------------------------------------------------------------------- 1 | package com.earldouglas.linearscala 2 | 3 | import sbt.Keys._ 4 | import sbt._ 5 | import sbt.plugins.JvmPlugin 6 | import scalafix.sbt.ScalafixPlugin 7 | 8 | object LinearScala extends AutoPlugin { 9 | 10 | override def requires = JvmPlugin && ScalafixPlugin 11 | override def trigger = allRequirements 12 | 13 | override val buildSettings: Seq[Def.Setting[_]] = 14 | Seq( 15 | ScalafixPlugin.autoImport.scalafixDependencies += 16 | "com.earldouglas" %% "linear-scala-scalafix" % LinearScalaBuildInfo.version, 17 | semanticdbEnabled := true, 18 | semanticdbVersion := ScalafixPlugin.autoImport.scalafixSemanticdb.revision 19 | ) 20 | 21 | override val projectSettings: Seq[Def.Setting[_]] = 22 | Seq( 23 | libraryDependencies += 24 | "com.earldouglas" % "linear-scala" % LinearScalaBuildInfo.version, 25 | ScalafixPlugin.autoImport.scalafixOnCompile := true 26 | ) ++ Seq(Compile, Test).map { c => 27 | c / ScalafixPlugin.autoImport.scalafix := 28 | (c / ScalafixPlugin.autoImport.scalafix) 29 | .partialInput(" LinearTypes") 30 | .evaluated 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /plugin/src/sbt-test/linear-scala/simple/project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin( 2 | "com.earldouglas" % "sbt-linear-scala" % sys.props("plugin.version") 3 | ) 4 | -------------------------------------------------------------------------------- /plugin/src/sbt-test/linear-scala/simple/src/main/examples/FieldUsedTwice.scala: -------------------------------------------------------------------------------- 1 | import com.earldouglas.linearscala.Linear 2 | 3 | case class Box(value: Int) extends Linear 4 | 5 | trait FieldUsedTwice { 6 | val box: Box = Box(42) 7 | println(box) // error: box is used twice 8 | println(box) // error: box is used twice 9 | } 10 | -------------------------------------------------------------------------------- /plugin/src/sbt-test/linear-scala/simple/src/main/examples/Linear.scala: -------------------------------------------------------------------------------- 1 | import com.earldouglas.linearscala.Linear 2 | 3 | case class Box(value: Int) extends Linear 4 | 5 | object IsLinear extends App { 6 | def identity(x: Box): Box = x 7 | println(identity(Box(42))) 8 | } 9 | -------------------------------------------------------------------------------- /plugin/src/sbt-test/linear-scala/simple/src/main/examples/UnusedField.scala: -------------------------------------------------------------------------------- 1 | import com.earldouglas.linearscala.Linear 2 | 3 | case class Box(value: Int) extends Linear 4 | 5 | trait UnusedField { 6 | val box: Box = Box(42) // error: box is never used 7 | } 8 | -------------------------------------------------------------------------------- /plugin/src/sbt-test/linear-scala/simple/src/main/examples/UnusedParameter.scala: -------------------------------------------------------------------------------- 1 | import com.earldouglas.linearscala.Linear 2 | 3 | case class Box(value: Int) extends Linear 4 | 5 | trait UnusedParameter { 6 | def foo(x: Box, y: Box): Int = // error: y is never used 7 | x.value 8 | } 9 | -------------------------------------------------------------------------------- /plugin/src/sbt-test/linear-scala/simple/test: -------------------------------------------------------------------------------- 1 | > clean 2 | $ copy src/main/examples/Linear.scala src/main/scala/ 3 | > compile 4 | $ delete src/main/scala/ 5 | 6 | > clean 7 | $ copy src/main/examples/FieldUsedTwice.scala src/main/scala/ 8 | -> compile 9 | $ delete src/main/scala/ 10 | 11 | > clean 12 | $ copy src/main/examples/UnusedField.scala src/main/scala/ 13 | -> compile 14 | $ delete src/main/scala/ 15 | 16 | > clean 17 | $ copy src/main/examples/UnusedParameter.scala src/main/scala/ 18 | -> compile 19 | $ delete src/main/scala/ 20 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.11.1 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.14.3") 2 | addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.11.1") 3 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.4") 4 | -------------------------------------------------------------------------------- /rules/src/main/resources/META-INF/services/scalafix.v1.Rule: -------------------------------------------------------------------------------- 1 | fix.LinearTypes 2 | -------------------------------------------------------------------------------- /rules/src/main/scala/fix/LinearTypes.scala: -------------------------------------------------------------------------------- 1 | package fix 2 | 3 | import scala.meta.Position 4 | import scala.meta.Term 5 | import scalafix.lint.Diagnostic 6 | import scalafix.v1._ 7 | 8 | case class MultipleDerefs(t: Term, count: Int)(implicit 9 | doc: SemanticDocument 10 | ) extends Diagnostic { 11 | override def position: Position = t.pos 12 | override def message: String = 13 | s"${LinearTypes.name(t)} is used ${count} times" 14 | } 15 | 16 | case class ZeroDerefs(t: Term)(implicit doc: SemanticDocument) 17 | extends Diagnostic { 18 | override def position: Position = t.pos 19 | override def message: String = 20 | s"${LinearTypes.name(t)} is never used" 21 | } 22 | 23 | object LinearTypes { 24 | def name(t: Term)(implicit doc: SemanticDocument): String = 25 | if (t.symbol.displayName.length > 0) { 26 | t.symbol.displayName 27 | } else { 28 | t.toString() 29 | } 30 | } 31 | 32 | final class LinearTypes extends SemanticRule("LinearTypes") { 33 | 34 | override val isLinter: Boolean = true 35 | override val isRewrite: Boolean = false 36 | 37 | // Try to figure out if the symbol is Linear 38 | private def isLinear( 39 | symbol: Symbol 40 | )(implicit doc: SemanticDocument): Boolean = 41 | symbol.value == "com/earldouglas/linearscala/Linear#" 42 | 43 | // Try to figure out if the type is a Linear 44 | private def isALinear( 45 | tpe: SemanticType 46 | )(implicit doc: SemanticDocument): Boolean = 47 | tpe match { 48 | case TypeRef(prefix, symbol, typeArguments) => isALinear(symbol) 49 | case StructuralType(WithType(types), declarations) => 50 | types.find(isALinear).isDefined 51 | case _ => false 52 | } 53 | 54 | // Try to figure out if the symbol represents a value that extends Linear 55 | private def isALinear( 56 | symbol: Symbol 57 | )(implicit doc: SemanticDocument): Boolean = 58 | isLinear(symbol) || symbol.info.map(isALinear).getOrElse(false) 59 | 60 | // Try to figure out if the symbol represents a value that extends Linear 61 | private def isALinear( 62 | info: SymbolInformation 63 | )(implicit doc: SemanticDocument): Boolean = 64 | info.signature match { 65 | case ClassSignature( 66 | typeParameters, 67 | parents, 68 | self, 69 | declarations 70 | ) => 71 | parents.find { 72 | case TypeRef(prefix, parentSymbol, typeArguments) => 73 | isLinear(parentSymbol) 74 | case _ => false 75 | } match { 76 | case Some(parent) => true 77 | case None => false 78 | } 79 | case ValueSignature(TypeRef(prefix, symbol, typeArguments)) => 80 | isALinear(symbol) 81 | case ValueSignature(tpe) => isALinear(tpe) 82 | case MethodSignature( 83 | typeParameters, 84 | parameterLists, 85 | returnType 86 | ) => 87 | isALinear(returnType) 88 | case _ => false 89 | } 90 | 91 | // Find all the terms that dereference a Linear value 92 | private def findDerefs(implicit 93 | doc: SemanticDocument 94 | ): Map[Symbol, List[Term]] = 95 | doc.tree 96 | .collect { 97 | case t @ Term.Name(name) 98 | if t.isReference && isALinear(t.symbol) => 99 | t 100 | } 101 | .groupBy(_.symbol) 102 | 103 | // Find all the terms that are references to a Linear value 104 | private def findRefs(implicit doc: SemanticDocument): Iterable[Term] = 105 | doc.tree.collect { 106 | case t @ Term.Name(name) 107 | if !t.isReference && isALinear(t.symbol) => 108 | t 109 | } 110 | 111 | override def fix(implicit doc: SemanticDocument): Patch = { 112 | val derefs: Map[Symbol, List[Term]] = findDerefs(doc) 113 | val refs: Iterable[Term] = findRefs(doc) 114 | 115 | // Find all Linear values that are used multiple times 116 | val overused: Iterable[Diagnostic] = 117 | derefs 118 | .filter(_._2.length > 1) 119 | .values 120 | .flatMap(ts => ts.map(t => MultipleDerefs(t, ts.length))) 121 | 122 | // Find all Linear values that are never used 123 | val underused: Iterable[Diagnostic] = 124 | refs 125 | .filterNot(t => derefs.keySet.contains(t.symbol)) 126 | .map(t => ZeroDerefs(t)) 127 | 128 | // Convert the above diagnostics into a Patch 129 | (overused ++ underused) 130 | .map(Patch.lint) 131 | .asPatch 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /tests/src/test/scala/fix/RuleSuite.scala: -------------------------------------------------------------------------------- 1 | package fix 2 | 3 | import scalafix.testkit._ 4 | import org.scalatest.funsuite.AnyFunSuiteLike 5 | 6 | class RuleSuite extends AbstractSemanticRuleSuite with AnyFunSuiteLike { 7 | runAllTests() 8 | } 9 | --------------------------------------------------------------------------------