├── .drone.yml ├── .drone.yml.sig ├── .gitignore ├── LICENSE.md ├── README.md ├── build.sbt ├── example └── src │ └── main │ └── scala │ └── demo │ └── Demo.scala ├── plugin └── src │ ├── main │ ├── resources │ │ └── scalac-plugin.xml │ ├── scala-2.11 │ │ └── io │ │ │ └── github │ │ │ └── retronym │ │ │ └── classpathshrinker │ │ │ └── Compat.scala │ ├── scala-2.12 │ │ └── io │ │ │ └── github │ │ │ └── retronym │ │ │ └── classpathshrinker │ │ │ └── Compat.scala │ └── scala │ │ └── io │ │ └── github │ │ └── retronym │ │ └── classpathshrinker │ │ ├── ClassPathFeedback.scala │ │ └── ClassPathShrinker.scala │ └── test │ └── scala │ └── io │ └── github │ └── retronym │ └── classpathshrinker │ ├── ClassPathShrinkerSpec.scala │ └── TestUtil.scala ├── project ├── build.properties └── plugins.sbt └── version.sbt /.drone.yml: -------------------------------------------------------------------------------- 1 | pipeline: 2 | sftp_cache: 3 | image: plugins/sftp-cache 4 | restore: true 5 | mount: 6 | - /drone/.ivy2 7 | - /drone/.coursier-cache 8 | - /drone/.sbt 9 | - /drone/.git 10 | 11 | build: 12 | image: scalacenter/scala-extras:1.0 13 | commands: 14 | - sbt "+plugin/test" "+example/compile" 15 | 16 | sftp_cache: 17 | image: plugins/sftp-cache 18 | rebuild: true 19 | mount: 20 | - /drone/.ivy2 21 | - /drone/.coursier-cache 22 | - /drone/.sbt 23 | - /drone/.git 24 | -------------------------------------------------------------------------------- /.drone.yml.sig: -------------------------------------------------------------------------------- 1 | eyJhbGciOiJIUzI1NiJ9.cGlwZWxpbmU6CiAgc2Z0cF9jYWNoZToKICAgIGltYWdlOiBwbHVnaW5zL3NmdHAtY2FjaGUKICAgIHJlc3RvcmU6IHRydWUKICAgIG1vdW50OgogICAgICAtIC9kcm9uZS8uaXZ5MgogICAgICAtIC9kcm9uZS8uY291cnNpZXItY2FjaGUKICAgICAgLSAvZHJvbmUvLnNidAogICAgICAtIC9kcm9uZS8uZ2l0CgogIGJ1aWxkOgogICAgaW1hZ2U6IHNjYWxhY2VudGVyL3NjYWxhLWV4dHJhczoxLjAKICAgIGNvbW1hbmRzOgogICAgICAtIHNidCAiK3BsdWdpbi90ZXN0IiAiK2V4YW1wbGUvY29tcGlsZSIKCiAgc2Z0cF9jYWNoZToKICAgIGltYWdlOiBwbHVnaW5zL3NmdHAtY2FjaGUKICAgIHJlYnVpbGQ6IHRydWUKICAgIG1vdW50OgogICAgICAtIC9kcm9uZS8uaXZ5MgogICAgICAtIC9kcm9uZS8uY291cnNpZXItY2FjaGUKICAgICAgLSAvZHJvbmUvLnNidAogICAgICAtIC9kcm9uZS8uZ2l0Cg.cVXV9KBuas-LiaDYqiUiu30feLTynGzu_jo80PIxdnw -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .DS_Store 3 | .idea/ 4 | .ensime 5 | .ensime_cache/ 6 | toolbox.classpath 7 | toolbox.plugin 8 | extra.classpath 9 | /.gitignore 10 | ### SBT template 11 | # Simple Build Tool 12 | # http://www.scala-sbt.org/release/docs/Getting-Started/Directories.html#configuring-version-control 13 | 14 | target/ 15 | lib_managed/ 16 | src_managed/ 17 | project/boot/ 18 | .history 19 | .cache 20 | ### Scala template 21 | *.class 22 | *.log 23 | 24 | # sbt specific 25 | .cache 26 | .history 27 | .lib/ 28 | dist/* 29 | target/ 30 | lib_managed/ 31 | src_managed/ 32 | project/boot/ 33 | project/plugins/project/ 34 | 35 | # Scala-IDE specific 36 | .scala_dependencies 37 | .worksheet 38 | 39 | ### JetBrains template 40 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 41 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 42 | 43 | # User-specific stuff: 44 | .idea/workspace.xml 45 | .idea/tasks.xml 46 | .idea/dictionaries 47 | .idea/vcs.xml 48 | .idea/jsLibraryMappings.xml 49 | 50 | # Sensitive or high-churn files: 51 | .idea/dataSources.ids 52 | .idea/dataSources.xml 53 | .idea/dataSources.local.xml 54 | .idea/sqlDataSources.xml 55 | .idea/dynamic.xml 56 | .idea/uiDesigner.xml 57 | 58 | # Gradle: 59 | .idea/gradle.xml 60 | .idea/libraries 61 | 62 | # Mongo Explorer plugin: 63 | .idea/mongoSettings.xml 64 | 65 | ## File-based project format: 66 | *.iws 67 | 68 | ## Plugin-specific files: 69 | 70 | # IntelliJ 71 | /out/ 72 | 73 | # mpeltonen/sbt-idea plugin 74 | .idea_modules/ 75 | 76 | # JIRA plugin 77 | atlassian-ide-plugin.xml 78 | 79 | # Crashlytics plugin (for Android Studio and IntelliJ) 80 | com_crashlytics_export_strings.xml 81 | crashlytics.properties 82 | crashlytics-build.properties 83 | fabric.properties 84 | 85 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2018 Martin Duhem 190 | Copyright 2018 Jorge Vicente Cantero 191 | Copyright 2018 EPFL (École Polytechnique Federal de Lausanne) 192 | 193 | Licensed under the Apache License, Version 2.0 (the "License"); 194 | you may not use this file except in compliance with the License. 195 | You may obtain a copy of the License at 196 | 197 | http://www.apache.org/licenses/LICENSE-2.0 198 | 199 | Unless required by applicable law or agreed to in writing, software 200 | distributed under the License is distributed on an "AS IS" BASIS, 201 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 202 | See the License for the specific language governing permissions and 203 | limitations under the License. 204 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Classpath Shrinker 2 | [![Build Status](https://platform-ci.scala-lang.org/api/badges/scalacenter/classpath-shrinker/status.svg)](https://platform-ci.scala-lang.org/scalacenter/classpath-shrinker) 3 | [![Maven Central](https://img.shields.io/maven-central/v/ch.epfl.scala/classpath-shrinker_2.12.svg)][search.maven] 4 | 5 | (This project is completed and currently not maintained by anyone) 6 | 7 | The Classpath Shrinker is a scalac plugin to detect unused classpath entries. 8 | It was originally created by [Jason Zaugg](https://github.com/retronym) as a better alternative to [a commit](https://github.com/jvican/scala/commit/8d22990ce32d9215f7e1fdd839f00f651b283744) 9 | which fulfilled the same functionality but required the instrumentation of symbol 10 | initializers. 11 | 12 | This plugin is now maintained by [the Scala Center](https://scala.epfl.ch). 13 | 14 | The creation of this plugin was motivated by [SCP-009: Improve direct dependency experience](https://github.com/scalacenter/advisoryboard/blob/master/proposals/009-improve-direct-dependency-experience.md), 15 | and complements the improvements to stub error messages [available in 2.12.2](https://github.com/scala/scala/pull/5724) 16 | and [2.11.9](https://github.com/scala/scala/issues/5804). 17 | 18 | If you use Pants or Bazel, you may find this compiler plugin useful. 19 | 20 | ### Add to your project 21 | 22 | ```scala 23 | resolvers += Resolver.bintrayRepo("scalacenter", "releases") 24 | addCompilerPlugin("ch.epfl.scala" %% "classpath-shrinker" % "0.1.1") 25 | ``` 26 | 27 | Once it's added, it will report if there are unused classpath entries automatically. 28 | 29 | Output looks like: 30 | 31 | ``` 32 | [info] Compiling 1 Scala source to /drone/src/github.com/scalacenter/classpath-shrinker/example/target/scala-2.12/classes... 33 | [warn] Detected the following unused classpath entries: 34 | [warn] /.coursier-cache/https/repo1.maven.org/maven2/com/google/guava/guava/21.0/guava-21.0.jar 35 | [warn] one warning found 36 | ``` 37 | 38 | [search.maven]: http://search.maven.org/#search|ga|1|ch.epfl.scala.classpath-shrinker 39 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | lazy val commonSettings = Seq( 2 | scalaVersion in ThisBuild := "2.12.1", 3 | crossScalaVersions in ThisBuild := Seq("2.11.8", "2.12.1"), 4 | organization in ThisBuild := "ch.epfl.scala" 5 | ) 6 | 7 | lazy val testDependencies = Seq( 8 | "junit" % "junit" % "4.12" % "test", 9 | "com.novocode" % "junit-interface" % "0.11" % "test", 10 | // Depend on coursier to resolve unused classpath entries 11 | "io.get-coursier" %% "coursier" % "1.0.0-M15" % "test", 12 | "io.get-coursier" %% "coursier-cache" % "1.0.0-M15" % "test" 13 | ) 14 | 15 | lazy val publishSettings = Seq( 16 | publishMavenStyle := true, 17 | bintrayOrganization := Some("scalacenter"), 18 | bintrayRepository := "releases", 19 | bintrayPackageLabels := Seq("scalac", 20 | "plugin", 21 | "scp-009", 22 | "direct", 23 | "dependency"), 24 | publishTo := (publishTo in bintray).value, 25 | publishArtifact in Test := false, 26 | licenses := Seq( 27 | "Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0") 28 | ), 29 | homepage := Some(url("https://github.com/scalacenter/classpath-shrinker")), 30 | autoAPIMappings := true, 31 | startYear := Some(2017), 32 | scmInfo := Some( 33 | ScmInfo( 34 | url("https://github.com/scalacenter/classpath-shrinker"), 35 | "scm:git:git@github.com:scalacenter/classpath-shrinker.git" 36 | ) 37 | ), 38 | developers := List( 39 | Developer("retronym", 40 | "Jason Zaugg", 41 | "jason.zaugg@lightbend.com", 42 | url("http://github.com/retronym")), 43 | Developer("jvican", 44 | "Jorge Vicente Cantero", 45 | "jorge.vicentecantero@epfl.ch", 46 | url("http://github.com/jvican")) 47 | ) 48 | ) 49 | 50 | lazy val noPublish = Seq( 51 | publish := {}, 52 | publishLocal := {} 53 | ) 54 | 55 | lazy val root = project.aggregate(plugin, example).settings(commonSettings) 56 | 57 | def inCompileAndTest(ss: Setting[_]*): Seq[Setting[_]] = 58 | Seq(Compile, Test).flatMap(inConfig(_)(ss)) 59 | 60 | val scalaPartialVersion = 61 | Def.setting(CrossVersion partialVersion scalaVersion.value) 62 | 63 | lazy val plugin = project.settings( 64 | name := "classpath-shrinker", 65 | scalaVersion in ThisBuild := "2.12.1", 66 | crossScalaVersions in ThisBuild := Seq("2.11.8", "2.12.1"), 67 | libraryDependencies += "org.scala-lang" % "scala-compiler" % scalaVersion.value, 68 | libraryDependencies ++= testDependencies, 69 | testOptions in Test ++= List(Tests.Argument("-v"), Tests.Argument("-s")), 70 | publishSettings, 71 | // Generate toolbox classpath while compiling 72 | resourceGenerators in Compile += generateToolboxClasspath.taskValue, 73 | resourceGenerators in Test += Def.task { 74 | val options = { 75 | val jar = (Keys.`package` in Compile).value 76 | val addPlugin = "-Xplugin:" + jar.getAbsolutePath 77 | val dummy = "-Jdummy=" + jar.lastModified 78 | Seq(addPlugin, dummy) 79 | } 80 | val stringOptions = options.filterNot(_ == "-Ydebug").mkString(" ") 81 | val resourceDir = (resourceDirectory in Test).value 82 | val pluginOptionsFile = resourceDir / "toolbox.plugin" 83 | IO.write(pluginOptionsFile, stringOptions) 84 | List(pluginOptionsFile.getAbsoluteFile) 85 | }.taskValue, 86 | inCompileAndTest(unmanagedSourceDirectories ++= { 87 | scalaPartialVersion.value.collect { 88 | case (2, y) if y == 11 => new File(scalaSource.value.getPath + "-2.11") 89 | case (2, y) if y >= 12 => new File(scalaSource.value.getPath + "-2.12") 90 | }.toList 91 | }) 92 | ) 93 | 94 | /* Write all the compile-time dependencies of the spores macro to a file, 95 | * in order to read it from the created Toolbox to run the neg tests. */ 96 | lazy val generateToolboxClasspath = Def.task { 97 | val scalaBinVersion = (scalaBinaryVersion in Compile).value 98 | val targetDir = (target in Compile).value 99 | val compiledClassesDir = targetDir / s"scala-$scalaBinVersion/classes" 100 | val testClassesDir = targetDir / s"scala-$scalaBinVersion/test-classes" 101 | val libraryJar = scalaInstance.value.libraryJar.getAbsolutePath 102 | val classpath = s"$compiledClassesDir:$testClassesDir:$libraryJar" 103 | val resourceDir = (resourceDirectory in Compile).value 104 | resourceDir.mkdir() // In case it doesn't exist 105 | val toolboxTestClasspath = resourceDir / "toolbox.classpath" 106 | IO.write(toolboxTestClasspath, classpath) 107 | List(toolboxTestClasspath.getAbsoluteFile) 108 | } 109 | 110 | // A regular module with the application code. 111 | lazy val example = project 112 | .settings( 113 | libraryDependencies += "org.apache.commons" % "commons-lang3" % "3.5", 114 | libraryDependencies += "com.google.guava" % "guava" % "21.0", 115 | noPublish, 116 | scalacOptions in Compile ++= { 117 | val jar = (Keys.`package` in (plugin, Compile)).value 118 | val addPlugin = "-Xplugin:" + jar.getAbsolutePath 119 | val dummy = "-Jdummy=" + jar.lastModified 120 | Seq(addPlugin, dummy) 121 | } 122 | ) 123 | -------------------------------------------------------------------------------- /example/src/main/scala/demo/Demo.scala: -------------------------------------------------------------------------------- 1 | package demo 2 | 3 | object Demo { 4 | org.apache.commons.lang3.ArrayUtils.EMPTY_BOOLEAN_ARRAY.length 5 | //com.google.common.base.Strings.commonPrefix("abc", "abcd") 6 | } 7 | -------------------------------------------------------------------------------- /plugin/src/main/resources/scalac-plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | classpath-shrinker 3 | io.github.retronym.classpathshrinker.ClassPathShrinker 4 | -------------------------------------------------------------------------------- /plugin/src/main/scala-2.11/io/github/retronym/classpathshrinker/Compat.scala: -------------------------------------------------------------------------------- 1 | package io.github.retronym.classpathshrinker 2 | 3 | import scala.tools.nsc.Settings 4 | import scala.tools.nsc.classpath.FlatClassPathFactory 5 | 6 | /** 7 | * Provides compatibility stubs for 2.11 and 2.12 Scala compilers. 8 | */ 9 | trait Compat { 10 | def getClassPathFrom(settings: Settings) = new FlatClassPathFactory(settings) 11 | } 12 | -------------------------------------------------------------------------------- /plugin/src/main/scala-2.12/io/github/retronym/classpathshrinker/Compat.scala: -------------------------------------------------------------------------------- 1 | package io.github.retronym.classpathshrinker 2 | 3 | import scala.tools.nsc.Settings 4 | import scala.tools.nsc.classpath.ClassPathFactory 5 | 6 | /** 7 | * Provides compatibility stubs for 2.11 and 2.12 Scala compilers. 8 | */ 9 | trait Compat { 10 | def getClassPathFrom(settings: Settings) = new ClassPathFactory(settings) 11 | } 12 | -------------------------------------------------------------------------------- /plugin/src/main/scala/io/github/retronym/classpathshrinker/ClassPathFeedback.scala: -------------------------------------------------------------------------------- 1 | package io.github.retronym.classpathshrinker 2 | 3 | object ClassPathFeedback { 4 | def createWarningMsg(unusedEntries: Seq[String]): String = { 5 | if (unusedEntries.isEmpty) "" 6 | else { 7 | val entries = unusedEntries.mkString("\n") 8 | s"Detected the following unused classpath entries: \n$entries" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /plugin/src/main/scala/io/github/retronym/classpathshrinker/ClassPathShrinker.scala: -------------------------------------------------------------------------------- 1 | package io.github.retronym.classpathshrinker 2 | 3 | import java.io.File 4 | import java.net.URI 5 | 6 | import scala.reflect.io.AbstractFile 7 | import scala.tools.nsc.plugins.{Plugin, PluginComponent} 8 | import scala.tools.nsc.{Global, Phase} 9 | 10 | class ClassPathShrinker(val global: Global) extends Plugin with Compat { 11 | 12 | val name = "classpath-shrinker" 13 | val description = 14 | "Warns about classpath entries that are not directly needed." 15 | val components = List[PluginComponent](Component) 16 | 17 | private object Component extends PluginComponent { 18 | val global: ClassPathShrinker.this.global.type = 19 | ClassPathShrinker.this.global 20 | import global._ 21 | 22 | override val runsAfter = List("jvm") 23 | 24 | val phaseName = ClassPathShrinker.this.name 25 | 26 | override def newPhase(prev: Phase): StdPhase = new StdPhase(prev) { 27 | override def run(): Unit = { 28 | super.run() 29 | val usedJars = findUsedJars 30 | val usedClasspathStrings = usedJars.toList.map(_.canonicalPath).sorted 31 | val userClasspath = getClassPathFrom(settings) 32 | val userClasspathURLs = userClasspath 33 | .classesInExpandedPath(settings.classpath.value) 34 | .flatMap(_.asURLs) 35 | def toJar(u: URI): Option[File] = 36 | util.Try { new File(u) }.toOption.filter(_.getName.endsWith(".jar")) 37 | val userClasspathStrings = 38 | userClasspathURLs.flatMap(x => toJar(x.toURI)).map(_.getCanonicalPath).toList 39 | val unneededClasspath = 40 | userClasspathStrings.filterNot(s => usedClasspathStrings.contains(s)) 41 | if (unneededClasspath.nonEmpty) { 42 | warning(ClassPathFeedback.createWarningMsg(unneededClasspath)) 43 | } 44 | } 45 | override def apply(unit: CompilationUnit): Unit = () 46 | } 47 | } 48 | 49 | import global._ 50 | 51 | private def findUsedJars: Set[AbstractFile] = { 52 | val jars = collection.mutable.Set[AbstractFile]() 53 | 54 | def walkTopLevels(root: Symbol): Unit = { 55 | def safeInfo(sym: Symbol): Type = 56 | if (sym.hasRawInfo && sym.rawInfo.isComplete) sym.info else NoType 57 | def packageClassOrSelf(sym: Symbol): Symbol = 58 | if (sym.hasPackageFlag && !sym.isModuleClass) sym.moduleClass else sym 59 | 60 | for (x <- safeInfo(packageClassOrSelf(root)).decls) { 61 | if (x == root) () 62 | else if (x.hasPackageFlag) walkTopLevels(x) 63 | else if (x.owner != root) { // exclude package class members 64 | if (x.hasRawInfo && x.rawInfo.isComplete) { 65 | val assocFile = x.associatedFile 66 | if (assocFile.path.endsWith(".class") && assocFile.underlyingSource.isDefined) 67 | assocFile.underlyingSource.foreach(jars += _) 68 | } 69 | } 70 | } 71 | } 72 | exitingTyper { 73 | walkTopLevels(RootClass) 74 | } 75 | jars.toSet 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /plugin/src/test/scala/io/github/retronym/classpathshrinker/ClassPathShrinkerSpec.scala: -------------------------------------------------------------------------------- 1 | package io.github.retronym.classpathshrinker 2 | 3 | import coursier.{Dependency, Module} 4 | import io.github.retronym.classpathshrinker.TestUtil._ 5 | import org.junit.Test 6 | import org.junit.runner.RunWith 7 | import org.junit.runners.JUnit4 8 | 9 | @RunWith(classOf[JUnit4]) 10 | class ClassPathShrinkerSpec { 11 | object Dependencies { 12 | val commons = 13 | Dependency(Module("org.apache.commons", "commons-lang3"), "3.5") 14 | val guava = Dependency(Module("com.google.guava", "guava"), "21.0") 15 | } 16 | 17 | import Dependencies._ 18 | 19 | @Test 20 | def `All is reported when nothing is used`(): Unit = { 21 | val testCode = 22 | """class A { 23 | | println("Hello") 24 | | scala.io.Source.fromFile("") 25 | |} 26 | """.stripMargin 27 | val unusedEntries = Coursier.getArtifacts(Seq(commons, guava)) 28 | val usedEntries = Seq() 29 | val expectedWarning = ClassPathFeedback.createWarningMsg(unusedEntries) 30 | val allEntries = usedEntries ++ unusedEntries 31 | expectWarning(expectedWarning, extraClasspath = allEntries)(testCode) 32 | } 33 | 34 | @Test 35 | def `Nothing is reported when everything is used`(): Unit = { 36 | val testCode = 37 | """object Demo { 38 | | org.apache.commons.lang3.ArrayUtils.EMPTY_BOOLEAN_ARRAY.length 39 | | com.google.common.base.Strings.commonPrefix("abc", "abcd") 40 | |} 41 | """.stripMargin 42 | val unusedEntries = Seq() 43 | val usedEntries = Coursier.getArtifacts(Seq(guava, commons)) 44 | val expectedWarning = ClassPathFeedback.createWarningMsg(unusedEntries) 45 | val allEntries = usedEntries ++ unusedEntries 46 | expectWarning(expectedWarning, extraClasspath = allEntries)(testCode) 47 | } 48 | 49 | @Test 50 | def `Commons is reported when guava is used`(): Unit = { 51 | val testCode = 52 | """object Demo1 { 53 | | com.google.common.base.Strings.commonPrefix("abc", "abcd") 54 | |} 55 | """.stripMargin 56 | val unusedEntries = Coursier.getArtifacts(Seq(commons)) 57 | val usedEntries = Coursier.getArtifacts(Seq(guava)) 58 | val expectedWarning = ClassPathFeedback.createWarningMsg(unusedEntries) 59 | val allEntries = usedEntries ++ unusedEntries 60 | expectWarning(expectedWarning, extraClasspath = allEntries)(testCode) 61 | } 62 | 63 | @Test 64 | def `Guava is reported when commons is used`(): Unit = { 65 | val testCode = 66 | """object Demo { 67 | | org.apache.commons.lang3.ArrayUtils.EMPTY_BOOLEAN_ARRAY.length 68 | |} 69 | """.stripMargin 70 | val unusedEntries = Coursier.getArtifacts(Seq(guava)) 71 | val usedEntries = Coursier.getArtifacts(Seq(commons)) 72 | val expectedWarning = ClassPathFeedback.createWarningMsg(unusedEntries) 73 | val allEntries = usedEntries ++ unusedEntries 74 | expectWarning(expectedWarning, extraClasspath = allEntries)(testCode) 75 | } 76 | 77 | @Test 78 | def `Nothing is reported when everything is used in a method`(): Unit = { 79 | val testCode = 80 | """object Demo { 81 | | def foo(): Unit = { 82 | | org.apache.commons.lang3.ArrayUtils.EMPTY_BOOLEAN_ARRAY.length 83 | | com.google.common.base.Strings.commonPrefix("abc", "abcd") 84 | | } 85 | |} 86 | """.stripMargin 87 | val unusedEntries = Seq() 88 | val usedEntries = Coursier.getArtifacts(Seq(guava, commons)) 89 | val expectedWarning = ClassPathFeedback.createWarningMsg(unusedEntries) 90 | val allEntries = usedEntries ++ unusedEntries 91 | expectWarning(expectedWarning, extraClasspath = allEntries)(testCode) 92 | } 93 | 94 | @Test 95 | def `Commons is reported when guava is used in a method`(): Unit = { 96 | val testCode = 97 | """object Demo1 { 98 | | def foo(): Unit = { 99 | | com.google.common.base.Strings.commonPrefix("abc", "abcd") 100 | | } 101 | |} 102 | """.stripMargin 103 | val unusedEntries = Coursier.getArtifacts(Seq(commons)) 104 | val usedEntries = Coursier.getArtifacts(Seq(guava)) 105 | val expectedWarning = ClassPathFeedback.createWarningMsg(unusedEntries) 106 | val allEntries = usedEntries ++ unusedEntries 107 | expectWarning(expectedWarning, extraClasspath = allEntries)(testCode) 108 | } 109 | 110 | @Test 111 | def `Guava is reported when commons is used in a method`(): Unit = { 112 | val testCode = 113 | """object Demo { 114 | | def foo(): Unit = { 115 | | org.apache.commons.lang3.ArrayUtils.EMPTY_BOOLEAN_ARRAY.length 116 | | } 117 | |} 118 | """.stripMargin 119 | val unusedEntries = Coursier.getArtifacts(Seq(guava)) 120 | val usedEntries = Coursier.getArtifacts(Seq(commons)) 121 | val expectedWarning = ClassPathFeedback.createWarningMsg(unusedEntries) 122 | val allEntries = usedEntries ++ unusedEntries 123 | expectWarning(expectedWarning, extraClasspath = allEntries)(testCode) 124 | } 125 | 126 | @Test 127 | def `Nothing is reported when everything is used in nested decl`(): Unit = { 128 | val testCode = 129 | """object Foo { 130 | | class Bar { 131 | | org.apache.commons.lang3.ArrayUtils.EMPTY_BOOLEAN_ARRAY.length 132 | | com.google.common.base.Strings.commonPrefix("abc", "abcd") 133 | | } 134 | |} 135 | """.stripMargin 136 | val unusedEntries = Seq() 137 | val usedEntries = Coursier.getArtifacts(Seq(guava, commons)) 138 | val expectedWarning = ClassPathFeedback.createWarningMsg(unusedEntries) 139 | val allEntries = usedEntries ++ unusedEntries 140 | expectWarning(expectedWarning, extraClasspath = allEntries)(testCode) 141 | } 142 | 143 | @Test 144 | def `Commons is reported when guava is used in nested decl`(): Unit = { 145 | val testCode = 146 | """object Demo1 { 147 | | class Bar { 148 | | com.google.common.base.Strings.commonPrefix("abc", "abcd") 149 | | } 150 | |} 151 | """.stripMargin 152 | val unusedEntries = Coursier.getArtifacts(Seq(commons)) 153 | val usedEntries = Coursier.getArtifacts(Seq(guava)) 154 | val expectedWarning = ClassPathFeedback.createWarningMsg(unusedEntries) 155 | val allEntries = usedEntries ++ unusedEntries 156 | expectWarning(expectedWarning, extraClasspath = allEntries)(testCode) 157 | } 158 | 159 | @Test 160 | def `Guava is reported when commons is used in nested decl`(): Unit = { 161 | val testCode = 162 | """object Demo { 163 | | class Bar { 164 | | org.apache.commons.lang3.ArrayUtils.EMPTY_BOOLEAN_ARRAY.length 165 | | } 166 | |} 167 | """.stripMargin 168 | val unusedEntries = Coursier.getArtifacts(Seq(guava)) 169 | val usedEntries = Coursier.getArtifacts(Seq(commons)) 170 | val expectedWarning = ClassPathFeedback.createWarningMsg(unusedEntries) 171 | val allEntries = usedEntries ++ unusedEntries 172 | expectWarning(expectedWarning, extraClasspath = allEntries)(testCode) 173 | } 174 | 175 | @Test 176 | def `Nothing is reported when everything is used in nested pkg`(): Unit = { 177 | val testCode = 178 | """package demo { 179 | | class Bar { 180 | | org.apache.commons.lang3.ArrayUtils.EMPTY_BOOLEAN_ARRAY.length 181 | | com.google.common.base.Strings.commonPrefix("abc", "abcd") 182 | | } 183 | |} 184 | """.stripMargin 185 | val unusedEntries = Seq() 186 | val usedEntries = Coursier.getArtifacts(Seq(guava, commons)) 187 | val expectedWarning = ClassPathFeedback.createWarningMsg(unusedEntries) 188 | val allEntries = usedEntries ++ unusedEntries 189 | expectWarning(expectedWarning, extraClasspath = allEntries)(testCode) 190 | } 191 | 192 | @Test 193 | def `Commons is reported when guava is used in nested pkg`(): Unit = { 194 | val testCode = 195 | """package demo { 196 | | class Bar { 197 | | com.google.common.base.Strings.commonPrefix("abc", "abcd") 198 | | } 199 | |} 200 | """.stripMargin 201 | val unusedEntries = Coursier.getArtifacts(Seq(commons)) 202 | val usedEntries = Coursier.getArtifacts(Seq(guava)) 203 | val expectedWarning = ClassPathFeedback.createWarningMsg(unusedEntries) 204 | val allEntries = usedEntries ++ unusedEntries 205 | expectWarning(expectedWarning, extraClasspath = allEntries)(testCode) 206 | } 207 | 208 | @Test 209 | def `Guava is reported when commons is used in nested pkg`(): Unit = { 210 | val testCode = 211 | """package demo { 212 | | class Bar { 213 | | org.apache.commons.lang3.ArrayUtils.EMPTY_BOOLEAN_ARRAY.length 214 | | } 215 | |} 216 | """.stripMargin 217 | val unusedEntries = Coursier.getArtifacts(Seq(guava)) 218 | val usedEntries = Coursier.getArtifacts(Seq(commons)) 219 | val expectedWarning = ClassPathFeedback.createWarningMsg(unusedEntries) 220 | val allEntries = usedEntries ++ unusedEntries 221 | expectWarning(expectedWarning, extraClasspath = allEntries)(testCode) 222 | } 223 | 224 | @Test 225 | def `Nothing is reported when everything is used in nested pkg object`(): Unit = { 226 | val testCode = 227 | """package object demo { 228 | | class Bar { 229 | | org.apache.commons.lang3.ArrayUtils.EMPTY_BOOLEAN_ARRAY.length 230 | | com.google.common.base.Strings.commonPrefix("abc", "abcd") 231 | | } 232 | |} 233 | """.stripMargin 234 | val unusedEntries = Seq() 235 | val usedEntries = Coursier.getArtifacts(Seq(guava, commons)) 236 | val expectedWarning = ClassPathFeedback.createWarningMsg(unusedEntries) 237 | val allEntries = usedEntries ++ unusedEntries 238 | expectWarning(expectedWarning, extraClasspath = allEntries)(testCode) 239 | } 240 | 241 | @Test 242 | def `Commons is reported when guava is used in nested pkg object`(): Unit = { 243 | val testCode = 244 | """package object demo { 245 | | class Bar { 246 | | com.google.common.base.Strings.commonPrefix("abc", "abcd") 247 | | } 248 | |} 249 | """.stripMargin 250 | val unusedEntries = Coursier.getArtifacts(Seq(commons)) 251 | val usedEntries = Coursier.getArtifacts(Seq(guava)) 252 | val expectedWarning = ClassPathFeedback.createWarningMsg(unusedEntries) 253 | val allEntries = usedEntries ++ unusedEntries 254 | expectWarning(expectedWarning, extraClasspath = allEntries)(testCode) 255 | } 256 | 257 | @Test 258 | def `Guava is reported when commons is used in nested pkg object`(): Unit = { 259 | val testCode = 260 | """package object demo { 261 | | class Bar { 262 | | org.apache.commons.lang3.ArrayUtils.EMPTY_BOOLEAN_ARRAY.length 263 | | } 264 | |} 265 | """.stripMargin 266 | val unusedEntries = Coursier.getArtifacts(Seq(guava)) 267 | val usedEntries = Coursier.getArtifacts(Seq(commons)) 268 | val expectedWarning = ClassPathFeedback.createWarningMsg(unusedEntries) 269 | val allEntries = usedEntries ++ unusedEntries 270 | expectWarning(expectedWarning, extraClasspath = allEntries)(testCode) 271 | } 272 | 273 | @Test 274 | def `Nothing is reported even if commons has relative path`(): Unit = { 275 | val testCode = 276 | """package object demo { 277 | | class Bar { 278 | | org.apache.commons.lang3.ArrayUtils.EMPTY_BOOLEAN_ARRAY.length 279 | | } 280 | |} 281 | """.stripMargin 282 | val unusedEntries = Seq() 283 | val usedEntries = Coursier.getArtifactsRelative(Seq(commons)) 284 | val expectedWarning = ClassPathFeedback.createWarningMsg(unusedEntries) 285 | val allEntries = usedEntries ++ unusedEntries 286 | expectWarning(expectedWarning, extraClasspath = allEntries)(testCode) 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /plugin/src/test/scala/io/github/retronym/classpathshrinker/TestUtil.scala: -------------------------------------------------------------------------------- 1 | package io.github.retronym.classpathshrinker 2 | 3 | import java.io.File 4 | import java.nio.file.Paths 5 | 6 | import coursier.maven.MavenRepository 7 | import coursier.{Cache, Dependency, Fetch, Resolution} 8 | 9 | import scala.reflect.internal.util.{BatchSourceFile, NoPosition} 10 | import scala.reflect.io.VirtualDirectory 11 | import scala.tools.cmd.CommandLineParser 12 | import scala.tools.nsc.reporters.StoreReporter 13 | import scala.tools.nsc.{CompilerCommand, Global, Settings} 14 | import scalaz.concurrent.Task 15 | 16 | object TestUtil { 17 | import scala.language.postfixOps 18 | 19 | /** Evaluate using global instance instead of toolbox because toolbox seems 20 | * to fail to typecheck code that comes from external dependencies. */ 21 | def eval(code: String, compileOptions: String = ""): StoreReporter = { 22 | // TODO: Optimize and cache global. 23 | val options = CommandLineParser.tokenize(compileOptions) 24 | val reporter = new StoreReporter() 25 | val settings = new Settings(println) 26 | val _ = new CompilerCommand(options, settings) 27 | settings.outputDirs.setSingleOutput(new VirtualDirectory("(memory)", None)) 28 | val global = new Global(settings, reporter) 29 | val run = new global.Run 30 | val toCompile = new BatchSourceFile("", code) 31 | run.compileSources(List(toCompile)) 32 | reporter 33 | } 34 | 35 | def getResourceContent(resourceName: String): String = { 36 | val resource = getClass.getClassLoader.getResource(resourceName) 37 | val file = scala.io.Source.fromFile(resource.toURI) 38 | file.getLines.mkString 39 | } 40 | 41 | lazy val toolboxClasspath: String = getResourceContent("toolbox.classpath") 42 | lazy val toolboxPluginOptions: String = getResourceContent("toolbox.plugin") 43 | 44 | def createBasicCompileOptions(classpath: String, usePluginOptions: String) = 45 | s"-classpath $classpath $usePluginOptions" 46 | 47 | def existsWarning(expectedWarning: String, 48 | reporter: StoreReporter): Boolean = { 49 | def hasDetectionWarning: Boolean = { 50 | reporter.infos.exists { info => 51 | info.severity.id == reporter.WARNING.id && 52 | info.msg.startsWith("Detected the following unused classpath entries") 53 | } 54 | } 55 | 56 | reporter.infos.exists { info => 57 | info.severity.id == reporter.WARNING.id && info.msg == expectedWarning 58 | } || (expectedWarning.isEmpty && !hasDetectionWarning) 59 | } 60 | 61 | def prettyPrintErrors(reporter: StoreReporter): String = { 62 | reporter.infos 63 | .map { info => 64 | if (info.pos == NoPosition) info.msg 65 | else s"""[${info.pos.source}]:${info.pos.line}: ${info.msg}""" 66 | } 67 | .mkString("\n") 68 | } 69 | 70 | def expectWarning(expectedWarning: String, 71 | compileOptions: String = "", 72 | extraClasspath: Seq[String])(code: String): Unit = { 73 | val fullClasspath: String = { 74 | val extraClasspathString = extraClasspath.mkString(":") 75 | if (toolboxClasspath.isEmpty) extraClasspathString 76 | else s"$toolboxClasspath:$extraClasspathString" 77 | } 78 | val basicOptions = 79 | createBasicCompileOptions(fullClasspath, toolboxPluginOptions) 80 | val reporter = eval(code, s"$basicOptions $compileOptions") 81 | assert( 82 | existsWarning(expectedWarning, reporter), { 83 | val errors = prettyPrintErrors(reporter) 84 | s"Expected warning does not exist." + 85 | s"Found:\n$errors\nExpected:\n$expectedWarning" 86 | } 87 | ) 88 | } 89 | 90 | object Coursier { 91 | private final val repositories = Seq( 92 | Cache.ivy2Local, 93 | MavenRepository("https://repo1.maven.org/maven2") 94 | ) 95 | 96 | def getArtifacts(deps: Seq[Dependency]): Seq[String] = 97 | getArtifacts(deps, toAbsolutePath) 98 | 99 | def getArtifactsRelative(deps: Seq[Dependency]): Seq[String] = 100 | getArtifacts(deps, toRelativePath) 101 | 102 | private def getArtifacts(deps: Seq[Dependency], fileToString: File => String): Seq[String] = { 103 | val toResolve = Resolution(deps.toSet) 104 | val fetch = Fetch.from(repositories, Cache.fetch()) 105 | val resolution = toResolve.process.run(fetch).run 106 | val resolutionErrors = resolution.errors 107 | if (resolutionErrors.nonEmpty) 108 | sys.error(s"Modules could not be resolved:\n$resolutionErrors.") 109 | val errorsOrJars = Task 110 | .gatherUnordered(resolution.artifacts.map(Cache.file(_).run)) 111 | .unsafePerformSync 112 | val onlyErrors = errorsOrJars.filter(_.isLeft) 113 | if (onlyErrors.nonEmpty) 114 | sys.error(s"Jars could not be fetched from cache:\n$onlyErrors") 115 | errorsOrJars.flatMap(_.map(fileToString).toList) 116 | } 117 | 118 | private def toAbsolutePath(f: File): String = 119 | f.getAbsolutePath 120 | 121 | private def toRelativePath(f: File): String = 122 | Paths.get(System.getProperty("user.dir")).relativize(f.toPath).toString 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=0.13.13 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("me.lessis" % "bintray-sbt" % "0.3.0") 2 | addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0-M15") 3 | addSbtPlugin("com.eed3si9n" % "sbt-doge" % "0.1.5") 4 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0") 5 | -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | version in ThisBuild := "0.1.1" 2 | --------------------------------------------------------------------------------