├── .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 | [](https://platform-ci.scala-lang.org/scalacenter/classpath-shrinker)
3 | [][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 |
--------------------------------------------------------------------------------