├── .github └── workflows │ ├── ci.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.sbt ├── docs └── README.md ├── project ├── build.properties └── plugins.sbt └── src ├── main ├── resources │ ├── plugin.properties │ └── scalac-plugin.xml ├── scala-2 │ └── holes │ │ ├── NamedHolesComponent.scala │ │ ├── TypedHolesComponent.scala │ │ └── TypedHolesPlugin.scala ├── scala-3 │ └── holes │ │ ├── NamedHolesPhase.scala │ │ ├── TypedHolesPhase.scala │ │ └── TypedHolesPlugin.scala └── scala │ └── holes │ ├── HoleName.scala │ └── LogLevel.scala └── test ├── resources ├── arguments │ ├── expected-3.3.txt │ ├── expected-3.txt │ └── input.scala ├── if-else │ ├── expected-3.txt │ ├── expected.txt │ └── input.scala ├── named-args │ ├── expected-3.txt │ └── input.scala ├── named-holes │ ├── expected-3.txt │ ├── expected.txt │ └── input.scala ├── nested-defs │ ├── expected-3.txt │ ├── expected.txt │ └── input.scala ├── pattern-match-on-either │ ├── expected-3.txt │ ├── expected.txt │ └── input.scala ├── pattern-match-on-nested-case-class │ ├── expected-3.txt │ ├── expected.txt │ └── input.scala ├── pattern-match │ ├── expected-3.txt │ ├── expected.txt │ └── input.scala ├── polymorphic │ ├── expected-3.txt │ ├── expected.txt │ └── input.scala ├── simple │ ├── expected-3.txt │ ├── expected.txt │ └── input.scala ├── type-ascription │ ├── expected-3.txt │ └── input.scala └── val-var-def-one-liners │ ├── expected-3.txt │ ├── expected.txt │ └── input.scala └── scala └── holes └── IntegrationTests.scala /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | 8 | jobs: 9 | main: 10 | name: Build and test 11 | strategy: 12 | fail-fast: false 13 | matrix: 14 | scala: 15 | - 2.11.12 16 | - 2.12.15 17 | - 2.13.13 18 | - 2.13.14 19 | - 2.13.15 20 | - 3.3.4 21 | - 3.4.3 22 | - 3.5.2 23 | - 3.6.2 24 | java: 25 | - 11 26 | - 17 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v2 31 | with: 32 | fetch-depth: 0 33 | 34 | - name: Setup Java and Scala 35 | uses: actions/setup-java@v4 36 | with: 37 | distribution: 'adopt' 38 | java-version: ${{ matrix.java }} 39 | cache: 'sbt' 40 | 41 | - name: Test 42 | run: | 43 | sbt ++${{ matrix.scala }} test 44 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | tags: ["v*"] 7 | 8 | jobs: 9 | publish: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v2 14 | with: 15 | fetch-depth: 0 # fetch all tags to make sbt-dynver work properly 16 | 17 | - name: Setup Java and Scala 18 | uses: actions/setup-java@v4 19 | with: 20 | distribution: 'adopt' 21 | java-version: 17 22 | cache: 'sbt' 23 | 24 | - name: Publish 25 | run: sbt ci-release 26 | env: 27 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 28 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 29 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 30 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 31 | 32 | update-readme: 33 | if: github.ref_type == 'tag' 34 | needs: publish 35 | runs-on: ubuntu-latest 36 | steps: 37 | - name: Checkout the tag 38 | uses: actions/checkout@v2 39 | with: 40 | fetch-depth: 0 # fetch all tags to make sbt-dynver work properly 41 | 42 | - name: Setup Java and Scala 43 | uses: actions/setup-java@v4 44 | with: 45 | distribution: 'adopt' 46 | java-version: 17 47 | cache: 'sbt' 48 | 49 | - name: Generate readme 50 | run: sbt docs/mdoc 51 | 52 | - name: Checkout the main branch into a subdirectory 53 | uses: actions/checkout@v2 54 | with: 55 | ref: main 56 | path: './main' 57 | 58 | - name: Copy generated readme to main branch 59 | run: cp README.md main 60 | 61 | - name: Commit and push readme 62 | uses: EndBug/add-and-commit@v8 63 | with: 64 | cwd: './main' 65 | add: 'README.md' 66 | default_author: github_actions 67 | message: 'Regenerate readme' 68 | 69 | 70 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/target 2 | .* 3 | !.gitignore 4 | **/metals.sbt -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 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 [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # scala-typed-holes 2 | 3 | This is a Scala compiler plugin to emulate the "typed holes" feature of Haskell, 4 | Idris, Agda, etc. 5 | 6 | Whenever you use `???` in your code, the compiler plugin will generate a 7 | compiler warning containing useful information about it. 8 | 9 | For example, given the Scala code 10 | 11 | ```scala 12 | package example 13 | 14 | object Example { 15 | 16 | def foo(x: Int, y: String): Boolean = { 17 | if (y.length == x) { 18 | ??? // TODO implement! 19 | } else { 20 | true 21 | } 22 | } 23 | 24 | def bar(x: Int): String = x match { 25 | case 0 => "zero" 26 | case 1 => "one" 27 | case _ => ??? 28 | } 29 | 30 | } 31 | ``` 32 | 33 | you'll get warnings that look something like this: 34 | 35 | ``` 36 | [warn] /Users/chris/code/scala-typed-holes/src/test/scala/example/Example.scala:7:7: 37 | [warn] Found hole with type: Boolean 38 | [warn] Relevant bindings include 39 | [warn] x: Int (bound at Example.scala:5:11) 40 | [warn] y: String (bound at Example.scala:5:19) 41 | [warn] 42 | [warn] ??? // TODO implement! 43 | [warn] ^ 44 | [warn] /Users/chris/code/scala-typed-holes/src/test/scala/example/Example.scala:16:15: 45 | [warn] Found hole with type: String 46 | [warn] Relevant bindings include 47 | [warn] x: Int (bound at Example.scala:13:11) 48 | [warn] 49 | [warn] case _ => ??? 50 | [warn] ^ 51 | ``` 52 | 53 | ## Named holes 54 | 55 | The plugin also supports named holes. Instead of using `???`, you can give 56 | custom names to your holes. 57 | 58 | For example, code like this 59 | 60 | ```scala 61 | def hello(args: Array[String]): Option[Result] = Foo.doStuff(args) match { 62 | case Left(error) => __left 63 | case Right(x) => __right 64 | } 65 | ``` 66 | 67 | will result in warnings like this 68 | 69 | ``` 70 | Found hole 'left' with type: Option[Result] 71 | Relevant bindings include 72 | args: Array[String] (bound at input.scala:11:13) 73 | error: String (bound at input.scala:12:15) 74 | 75 | case Left(error) => __left 76 | ^ 77 | ``` 78 | 79 | Named holes must start with a double underscore. 80 | 81 | Warning: if you happen to use a naming convention that includes double 82 | underscores (which is pretty rare in Scala), this plugin will probably trash 83 | your code! 84 | 85 | ## How to use 86 | 87 | In sbt: 88 | 89 | ``` 90 | addCompilerPlugin("com.github.cb372" % "scala-typed-holes" % "0.2.0" cross CrossVersion.full) 91 | ``` 92 | 93 | The plugin is published for the following Scala versions: 94 | 95 | * 2.11.12 96 | * 2.12.15 97 | * 2.13.{13, 14, 15} 98 | * 3.3.4 99 | * 3.4.3 100 | * 3.5.2 101 | * 3.6.2 102 | 103 | ## Changing the log level 104 | 105 | By passing a compiler option `-P:typed-holes:log-level:`, you can control 106 | the severity with which holes are logged. 107 | 108 | * `info` means holes will be logged as informational messages 109 | * `warn` means holes will be logged as compiler warnings 110 | * `error` means holes will be logged as compiler errors, so your program will 111 | fail to compile if it contains any holes. 112 | 113 | The default behaviour is to log holes as warnings. 114 | 115 | If you are using sbt, you can pass the option like this: 116 | 117 | ``` 118 | scalacOptions += "-P:typed-holes:log-level:info" 119 | ``` 120 | 121 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | import sbt.Keys._ 2 | import sbt._ 3 | 4 | scalaVersion := "2.13.4" 5 | val scala3 = "3.6.2" 6 | crossScalaVersions := List( 7 | "2.11.12", 8 | "2.12.15", 9 | "2.13.8", 10 | "2.13.9", 11 | "2.13.10", 12 | "2.13.11", 13 | "2.13.12", 14 | "2.13.13", 15 | "2.13.14", 16 | "2.13.15", 17 | "3.3.4", 18 | "3.4.3", 19 | "3.5.2", 20 | scala3 21 | ) 22 | 23 | crossVersion := CrossVersion.full 24 | scalacOptions ++= Seq("-deprecation") 25 | 26 | libraryDependencies ++= Seq( 27 | scalaVersion.value match { 28 | case version if version.startsWith("3.") => 29 | "org.scala-lang" %% "scala3-compiler" % scalaVersion.value 30 | case _ => 31 | scalaOrganization.value % "scala-compiler" % scalaVersion.value 32 | }, 33 | "org.scalatest" %% "scalatest" % "3.2.18" % Test, 34 | "commons-io" % "commons-io" % "2.6" % Test 35 | ) 36 | 37 | organization := "com.github.cb372" 38 | homepage := Some(url("https://github.com/cb372/scala-typed-holes")) 39 | licenses := Seq( 40 | "Apache License, Version 2.0" -> url( 41 | "http://www.apache.org/licenses/LICENSE-2.0.html" 42 | ) 43 | ) 44 | developers := List( 45 | Developer( 46 | "cb372", 47 | "Chris Birchall", 48 | "chris.birchall@gmail.com", 49 | url("https://twitter.com/cbirchall") 50 | ) 51 | ) 52 | 53 | Test / fork := true 54 | Test / javaOptions ++= { 55 | val jar = (Compile / packageBin).value 56 | val scalacClasspath = 57 | scalaInstance.value.allJars.mkString(java.io.File.pathSeparator) 58 | Seq( 59 | s"-Dplugin.jar=${jar.getAbsolutePath}", 60 | s"-Dscalac.classpath=$scalacClasspath" 61 | ) 62 | } 63 | 64 | val `scala-typed-holes` = 65 | project 66 | .in(file(".")) 67 | .enablePlugins(BuildInfoPlugin) 68 | 69 | val `scala-typed-holes-3` = // just for IDE support 70 | project 71 | .in(file("scala-typed-holes-3")) 72 | .settings( 73 | scalaVersion := scala3, 74 | crossScalaVersions := Nil, 75 | Compile / unmanagedSourceDirectories := Seq(), 76 | Compile / unmanagedSourceDirectories += (ThisBuild / baseDirectory).value / "src" / "main" / "scala", 77 | Compile / unmanagedSourceDirectories += (ThisBuild / baseDirectory).value / "src" / "main" / "scala-3", 78 | moduleName := "scala-typed-holes-3", 79 | target := (ThisBuild / baseDirectory).value / "target" / "target3", 80 | publish / skip := true, 81 | libraryDependencies ++= Seq( 82 | "org.scala-lang" %% "scala3-compiler" % scala3 83 | ), 84 | scalacOptions += "-Wunused:all" 85 | ) 86 | 87 | val docs = project 88 | .in( 89 | file("generated-docs") 90 | ) // important: it must not be the actual directory name, i.e. docs/ 91 | .settings( 92 | scalaVersion := "2.12.10", 93 | crossScalaVersions := Nil, 94 | publishArtifact := false, 95 | publish / skip := true, 96 | mdocVariables := Map("VERSION" -> version.value), 97 | mdocOut := file(".") 98 | ) 99 | .enablePlugins(MdocPlugin) 100 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # scala-typed-holes 2 | 3 | This is a Scala compiler plugin to emulate the "typed holes" feature of Haskell, 4 | Idris, Agda, etc. 5 | 6 | Whenever you use `???` in your code, the compiler plugin will generate a 7 | compiler warning containing useful information about it. 8 | 9 | For example, given the Scala code 10 | 11 | ```scala 12 | package example 13 | 14 | object Example { 15 | 16 | def foo(x: Int, y: String): Boolean = { 17 | if (y.length == x) { 18 | ??? // TODO implement! 19 | } else { 20 | true 21 | } 22 | } 23 | 24 | def bar(x: Int): String = x match { 25 | case 0 => "zero" 26 | case 1 => "one" 27 | case _ => ??? 28 | } 29 | 30 | } 31 | ``` 32 | 33 | you'll get warnings that look something like this: 34 | 35 | ``` 36 | [warn] /Users/chris/code/scala-typed-holes/src/test/scala/example/Example.scala:7:7: 37 | [warn] Found hole with type: Boolean 38 | [warn] Relevant bindings include 39 | [warn] x: Int (bound at Example.scala:5:11) 40 | [warn] y: String (bound at Example.scala:5:19) 41 | [warn] 42 | [warn] ??? // TODO implement! 43 | [warn] ^ 44 | [warn] /Users/chris/code/scala-typed-holes/src/test/scala/example/Example.scala:16:15: 45 | [warn] Found hole with type: String 46 | [warn] Relevant bindings include 47 | [warn] x: Int (bound at Example.scala:13:11) 48 | [warn] 49 | [warn] case _ => ??? 50 | [warn] ^ 51 | ``` 52 | 53 | ## Named holes 54 | 55 | The plugin also supports named holes. Instead of using `???`, you can give 56 | custom names to your holes. 57 | 58 | For example, code like this 59 | 60 | ```scala 61 | def hello(args: Array[String]): Option[Result] = Foo.doStuff(args) match { 62 | case Left(error) => __left 63 | case Right(x) => __right 64 | } 65 | ``` 66 | 67 | will result in warnings like this 68 | 69 | ``` 70 | Found hole 'left' with type: Option[Result] 71 | Relevant bindings include 72 | args: Array[String] (bound at input.scala:11:13) 73 | error: String (bound at input.scala:12:15) 74 | 75 | case Left(error) => __left 76 | ^ 77 | ``` 78 | 79 | Named holes must start with a double underscore. 80 | 81 | Warning: if you happen to use a naming convention that includes double 82 | underscores (which is pretty rare in Scala), this plugin will probably trash 83 | your code! 84 | 85 | ## How to use 86 | 87 | In sbt: 88 | 89 | ``` 90 | addCompilerPlugin("com.github.cb372" % "scala-typed-holes" % "@VERSION@" cross CrossVersion.full) 91 | ``` 92 | 93 | The plugin is published for the following Scala versions: 94 | 95 | * 2.11.12 96 | * 2.12.15 97 | * 2.13.{13, 14, 15} 98 | * 3.3.4 99 | * 3.4.3 100 | * 3.5.2 101 | * 3.6.2 102 | 103 | ## Changing the log level 104 | 105 | By passing a compiler option `-P:typed-holes:log-level:`, you can control 106 | the severity with which holes are logged. 107 | 108 | * `info` means holes will be logged as informational messages 109 | * `warn` means holes will be logged as compiler warnings 110 | * `error` means holes will be logged as compiler errors, so your program will 111 | fail to compile if it contains any holes. 112 | 113 | The default behaviour is to log holes as warnings. 114 | 115 | If you are using sbt, you can pass the option like this: 116 | 117 | ``` 118 | scalacOptions += "-P:typed-holes:log-level:info" 119 | ``` 120 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.9.9 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.scalameta" % "sbt-mdoc" % "1.3.1") 2 | addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.10") 3 | addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") 4 | -------------------------------------------------------------------------------- /src/main/resources/plugin.properties: -------------------------------------------------------------------------------- 1 | pluginClass=holes.TypedHolesPlugin 2 | -------------------------------------------------------------------------------- /src/main/resources/scalac-plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | typed-holes 3 | holes.TypedHolesPlugin 4 | 5 | -------------------------------------------------------------------------------- /src/main/scala-2/holes/NamedHolesComponent.scala: -------------------------------------------------------------------------------- 1 | package holes 2 | 3 | import scala.tools.nsc.{Global, Phase} 4 | import scala.tools.nsc.transform.Transform 5 | import scala.tools.nsc.ast.TreeDSL 6 | import scala.tools.nsc.plugins.{Plugin, PluginComponent} 7 | 8 | class NamedHolesComponent(plugin: Plugin, val global: Global) 9 | extends PluginComponent with TreeDSL with Transform { 10 | 11 | override val phaseName: String = "named-holes" 12 | override val runsAfter: List[String] = List("parser") 13 | override val runsBefore: List[String] = List("namer") 14 | 15 | import global._ 16 | 17 | override def newTransformer(unit: CompilationUnit): Transformer = 18 | new NamedHolesTransformer(unit) 19 | 20 | class NamedHolesTransformer(unit: CompilationUnit) extends Transformer { 21 | 22 | object NamedHole { 23 | val pattern = "^__([a-zA-Z0-9_]+)$".r 24 | 25 | def unapply(name: Name): Option[String] = 26 | pattern.unapplySeq(name.decoded.toString).flatMap(_.headOption) 27 | } 28 | 29 | override def transform(tree: Tree): Tree = { 30 | val t = super.transform(tree) 31 | t match { 32 | case Ident(NamedHole(name)) => 33 | atPos(t.pos)(treeCopy.Ident(t, TermName("$qmark$qmark$qmark"))) 34 | .updateAttachment(HoleName(name)) 35 | case _ => 36 | t 37 | } 38 | } 39 | 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/scala-2/holes/TypedHolesComponent.scala: -------------------------------------------------------------------------------- 1 | package holes 2 | 3 | import scala.collection.mutable 4 | import scala.tools.nsc.{Global, Phase} 5 | import scala.tools.nsc.ast.TreeDSL 6 | import scala.tools.nsc.plugins.{Plugin, PluginComponent} 7 | 8 | class TypedHolesComponent(plugin: Plugin, val global: Global, getLogLevel: () => LogLevel) 9 | extends PluginComponent with TreeDSL { 10 | 11 | override val phaseName: String = "typed-holes" 12 | override val runsAfter: List[String] = List("typer") 13 | override val runsBefore: List[String] = List("patmat") 14 | 15 | import global._ 16 | 17 | override def newPhase(prev: Phase): StdPhase = new StdPhase(prev) { 18 | override def apply(unit: CompilationUnit): Unit = { 19 | new TypedHolesTraverser(unit).traverse(unit.body) 20 | } 21 | } 22 | 23 | class TypedHolesTraverser(unit: CompilationUnit) extends Traverser { 24 | 25 | case class Binding(tpe: Type, pos: Position) 26 | 27 | private val bindings: mutable.ArrayStack[Map[TermName, Binding]] = new mutable.ArrayStack 28 | 29 | override def traverse(tree: Tree): Unit = { 30 | 31 | tree match { 32 | case ValDef(_, _, tpt, Hole(holeInRhs)) => 33 | log(holeInRhs, tpt.tpe) 34 | super.traverse(tree) 35 | case ValDef(_, _, tpt, Function(vparams, Hole(body))) => 36 | bindings.push(vparams.map(param => (param.name, Binding(param.tpt.tpe, param.pos))).toMap) 37 | log(body, tpt.tpe.typeArgs.last) 38 | super.traverse(tree) 39 | bindings.pop() 40 | case ValDef(_, _, _, _) => 41 | super.traverse(tree) 42 | case DefDef(_, _, _, vparamss, tpt, Hole(holeInRhs)) => 43 | bindings.push(vparamss.flatten.map(param => (param.name, Binding(param.tpt.tpe, param.pos))).toMap) 44 | log(holeInRhs, tpt.tpe) 45 | super.traverse(tree) 46 | bindings.pop() 47 | case DefDef(_, _, _, vparamss, _, _) => 48 | bindings.push(vparamss.flatten.map(param => (param.name, Binding(param.tpt.tpe, param.pos))).toMap) 49 | super.traverse(tree) 50 | bindings.pop() 51 | case Function(vparams, _) => 52 | bindings.push(vparams.map(param => (param.name, Binding(param.tpt.tpe, param.pos))).toMap) 53 | super.traverse(tree) 54 | bindings.pop() 55 | case If(_, Hole(a), Hole(b)) => 56 | log(a, tree.tpe) 57 | log(b, tree.tpe) 58 | super.traverse(tree) 59 | case If(_, Hole(a), _) => 60 | log(a, tree.tpe) 61 | super.traverse(tree) 62 | case If(_, _, Hole(b)) => 63 | log(b, tree.tpe) 64 | super.traverse(tree) 65 | case m @ Match(_, cases) => 66 | cases foreach { 67 | case CaseDef(pat, _, Hole(holeInBody)) => 68 | bindings.push(gatherPatternBindings(pat)) 69 | log(holeInBody, m.tpe) 70 | bindings.pop() 71 | case _ => 72 | } 73 | super.traverse(tree) 74 | case a @ Apply(_, args) => 75 | args foreach { 76 | case Function(vparams, Hole(body)) => 77 | bindings.push(vparams.map(param => (param.name, Binding(param.tpt.tpe, param.pos))).toMap) 78 | log(body, a.tpe) 79 | bindings.pop() 80 | case _ => 81 | } 82 | super.traverse(tree) 83 | case _ => 84 | super.traverse(tree) 85 | } 86 | 87 | } 88 | 89 | private def gatherPatternBindings(tree: Tree): Map[TermName, Binding] = tree match { 90 | case Bind(name, body) => 91 | Map(name.toTermName -> Binding(body.tpe, tree.pos)) ++ gatherPatternBindings(body) 92 | case Apply(_, args) => 93 | val bindingss = args.map { arg => 94 | val namedArgBinding = 95 | if (arg.symbol != NoSymbol) 96 | Map(arg.symbol.name.toTermName -> Binding(arg.tpe, arg.pos)) 97 | else 98 | Map.empty[TermName, Binding] 99 | 100 | val bindingsInsideArg = gatherPatternBindings(arg) 101 | 102 | namedArgBinding ++ bindingsInsideArg 103 | } 104 | bindingss.foldLeft(Map.empty[TermName, Binding])(_ ++ _) 105 | case _ => 106 | Map.empty 107 | } 108 | 109 | private def collectRelevantBindings: Map[TermName, Binding] = 110 | bindings.foldLeft(Map.empty[TermName, Binding]){ case (acc, level) => level ++ acc } 111 | 112 | private def log(holeTree: Tree, tpe: Type): Unit = { 113 | val relevantBindingsMessages = 114 | collectRelevantBindings.toList.sortBy(_._1.toString).map { 115 | case (boundName, Binding(boundType, bindingPos)) => s" $boundName: $boundType (bound at ${posSummary(bindingPos)})" 116 | } 117 | .mkString("\n") 118 | val holeName = holeTree.attachments.get[HoleName] 119 | val holeNameMsg = holeName.fold("")(x => s"'${x.name}' ") 120 | val message = 121 | if (!relevantBindingsMessages.isEmpty) 122 | s""" 123 | |Found hole ${holeNameMsg}with type: $tpe 124 | |Relevant bindings include 125 | |$relevantBindingsMessages 126 | """.stripMargin 127 | else 128 | s"Found hole with type: $tpe" 129 | getLogLevel() match { 130 | case LogLevel.Info => inform(holeTree.pos, message) 131 | case LogLevel.Warn => warning(holeTree.pos, message) 132 | case LogLevel.Error => globalError(holeTree.pos, message) 133 | } 134 | } 135 | 136 | private def posSummary(pos: Position): String = 137 | s"${pos.source.file.name}:${pos.line}:${pos.column}" 138 | 139 | object Hole { 140 | def unapply(tree: Tree): Option[Tree] = tree match { 141 | case _ if tree.symbol == definitions.Predef_??? => 142 | Some(tree) 143 | case Block(_, expr) if expr.symbol == definitions.Predef_??? => 144 | Some(expr) 145 | case _ => 146 | None 147 | } 148 | } 149 | 150 | } 151 | 152 | } 153 | 154 | -------------------------------------------------------------------------------- /src/main/scala-2/holes/TypedHolesPlugin.scala: -------------------------------------------------------------------------------- 1 | package holes 2 | 3 | import scala.collection.mutable 4 | import scala.tools.nsc.{Global, Phase} 5 | import scala.tools.nsc.transform.Transform 6 | import scala.tools.nsc.ast.TreeDSL 7 | import scala.tools.nsc.plugins.{Plugin, PluginComponent} 8 | 9 | class TypedHolesPlugin(val global: Global) extends Plugin { 10 | val name = "typed-holes" 11 | val description = "Treat use of ??? as a hole and give a useful warning about it" 12 | 13 | private var logLevel: LogLevel = LogLevel.Warn 14 | 15 | override def init(options: List[String], error: String => Unit): Boolean = { 16 | for (option <- options) { 17 | if (option.startsWith("log-level:")) { 18 | option.substring("log-level:".length).toLowerCase match { 19 | case "info" => 20 | logLevel = LogLevel.Info 21 | case "warn" => 22 | logLevel = LogLevel.Warn 23 | case "error" => 24 | logLevel = LogLevel.Error 25 | case other => 26 | error(s"Unexpected log level value: '$other'") 27 | } 28 | } else { 29 | error(s"Unrecognised option: $option") 30 | } 31 | } 32 | true 33 | } 34 | 35 | val components = List( 36 | new NamedHolesComponent(this, global), 37 | new TypedHolesComponent(this, global, () => logLevel) 38 | ) 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/main/scala-3/holes/NamedHolesPhase.scala: -------------------------------------------------------------------------------- 1 | package holes 2 | 3 | import dotty.tools.dotc.ast.tpd 4 | import dotty.tools.dotc.ast.untpd 5 | import dotty.tools.dotc.ast.untpd.UntypedTreeMap 6 | import dotty.tools.dotc.core.Contexts.Context 7 | import dotty.tools.dotc.core.Contexts.ctx 8 | import dotty.tools.dotc.core.Names.Name 9 | import dotty.tools.dotc.core.StdNames 10 | import dotty.tools.dotc.parsing.Parser 11 | import dotty.tools.dotc.plugins.PluginPhase 12 | import dotty.tools.dotc.typer.TyperPhase 13 | import dotty.tools.dotc.util.Property 14 | 15 | class NamedHolesPhase extends PluginPhase: 16 | override def phaseName: String = "named-holes" 17 | 18 | override val runsAfter: Set[String] = Set(Parser.name) 19 | override val runsBefore: Set[String] = Set(TyperPhase.name) 20 | 21 | override def prepareForUnit(tree: tpd.Tree)(using Context): Context = 22 | val namedHolesTreeMap = new UntypedTreeMap { 23 | override def transform(tree: tpd.Tree)(using Context): tpd.Tree = 24 | tree match 25 | case tpd.Ident(NamedHole(name)) => 26 | val copied = cpy.Ident(tree)(StdNames.nme.???) 27 | copied.putAttachment(NamedHole.NamedHole, HoleName(name)) 28 | copied 29 | case _ => 30 | super.transform(tree) 31 | } 32 | ctx.compilationUnit.untpdTree = 33 | namedHolesTreeMap.transform(ctx.compilationUnit.untpdTree) 34 | ctx 35 | 36 | object NamedHole: 37 | val NamedHole: Property.Key[HoleName] = Property.StickyKey() 38 | val pattern = "^__([a-zA-Z0-9_]+)$".r 39 | 40 | def unapply(name: Name): Option[String] = 41 | pattern.unapplySeq(name.decode.toString).flatMap(_.headOption) 42 | -------------------------------------------------------------------------------- /src/main/scala-3/holes/TypedHolesPhase.scala: -------------------------------------------------------------------------------- 1 | package holes 2 | 3 | import dotty.tools.dotc.ast.tpd 4 | import dotty.tools.dotc.core.Contexts.Context 5 | import dotty.tools.dotc.core.Contexts.ctx 6 | import dotty.tools.dotc.core.Names.TermName 7 | import dotty.tools.dotc.core.Symbols.defn 8 | import dotty.tools.dotc.core.Types.Type 9 | import dotty.tools.dotc.interfaces.SourcePosition 10 | import dotty.tools.dotc.plugins.PluginPhase 11 | import dotty.tools.dotc.printing.RefinedPrinter 12 | import dotty.tools.dotc.report 13 | import dotty.tools.dotc.reporting.Diagnostic.Info 14 | import dotty.tools.dotc.transform.PostTyper 15 | import dotty.tools.dotc.typer.TyperPhase 16 | 17 | import scala.collection.mutable 18 | import dotty.tools.dotc.core.Flags 19 | import dotty.tools.dotc.ast.Trees.Ident 20 | import dotty.tools.dotc.core.StdNames.nme 21 | import dotty.tools.dotc.core.Names.Name 22 | 23 | class TypedHolesPhase(logLevel: LogLevel) extends PluginPhase: 24 | override def phaseName: String = "typed-holes" 25 | override val runsAfter: Set[String] = Set(TyperPhase.name) 26 | override val runsBefore: Set[String] = Set(PostTyper.name) 27 | 28 | private val bindings: mutable.Stack[Map[TermName, Binding]] = 29 | mutable.Stack.empty 30 | 31 | private val syntheticVals: mutable.Set[Name] = mutable.Set.empty 32 | 33 | private val defaultWidth = 1000 34 | 35 | override def prepareForDefDef(tree: tpd.DefDef)(using Context): Context = 36 | bindings.push( 37 | tree.termParamss.flatten 38 | .map(param => (param.name, Binding(param.tpt.tpe, param.sourcePos))) 39 | .toMap 40 | ) 41 | ctx 42 | 43 | override def transformValDef(tree: tpd.ValDef)(using Context): tpd.Tree = 44 | tree match 45 | case tpd.ValDef(name, _, Ident(nme.???)) 46 | if tree.symbol.is(Flags.Synthetic) => 47 | syntheticVals += name 48 | case _ => logHole(tree.rhs, tree.tpt.tpe) 49 | tree 50 | 51 | override def transformDefDef(tree: tpd.DefDef)(using Context): tpd.Tree = 52 | logHole(tree.rhs, tree.tpt.tpe) 53 | bindings.pop() 54 | tree 55 | 56 | override def transformApply(tree: tpd.Apply)(using Context): tpd.Tree = 57 | val tpd.Apply(fun, args) = tree 58 | val index = paramIndex(fun) 59 | args 60 | .zip(fun.symbol.paramSymss(index)) 61 | .foreach: 62 | case (arg, param) => logHole(arg, param.info) 63 | tree 64 | 65 | override def transformTyped(tree: tpd.Typed)(using Context): tpd.Tree = 66 | logHole(tree.expr, tree.tpt.tpe) 67 | tree 68 | 69 | private def paramIndex(fun: tpd.Tree, index: Int = 0)(using Context): Int = 70 | fun match 71 | case tpd.Apply(f, _) if f.symbol == fun.symbol => paramIndex(f, index + 1) 72 | case _ => index 73 | 74 | private def logHole(holeTree: tpd.Tree, tpe: => Type)(using Context): Unit = 75 | holeTree match 76 | case Hole(holeInRhs) => log(holeInRhs, tpe.widen) 77 | case Ident(name) if syntheticVals(name) => 78 | syntheticVals -= name 79 | log(holeTree, tpe.widen) 80 | case tpd.Block(_, rhs) => logHole(rhs, tpe) 81 | case tpd.If(_, thenp, elsep) => 82 | logHole(thenp, tpe) 83 | logHole(elsep, tpe) 84 | case tpd.Match(_, caseDefs) => caseDefs.foreach(logHole(_, tpe)) 85 | case tpd.CaseDef(_, _, tree) => logHole(tree, tpe) 86 | case tpd.NamedArg(_, arg) => logHole(arg, tpe) 87 | case _ => 88 | 89 | private def collectRelevantBindings: Map[TermName, Binding] = 90 | bindings.foldLeft(Map.empty[TermName, Binding]) { case (acc, level) => 91 | level ++ acc 92 | } 93 | 94 | private def log(holeTree: tpd.Tree, tpe: Type)(using Context): Unit = { 95 | val printer = RefinedPrinter(ctx) 96 | def printType(tpe: Type) = 97 | printer.toText(tpe).mkString(defaultWidth, false) 98 | 99 | val relevantBindingsMessages = 100 | collectRelevantBindings.toList 101 | .sortBy(_._1.toString) 102 | .map: 103 | case (boundName, Binding(boundType, bindingPos)) => 104 | s" $boundName: ${printType(boundType)} (bound at ${posSummary(bindingPos)})" 105 | .mkString("\n") 106 | 107 | val holeName = holeTree.getAttachment(NamedHole.NamedHole) 108 | val holeNameMsg = holeName.fold("")(x => s"'${x.name}' ") 109 | val message = 110 | if (!relevantBindingsMessages.isEmpty) 111 | s""" 112 | |Found hole ${holeNameMsg}with type: ${printType(tpe)} 113 | |Relevant bindings include 114 | |$relevantBindingsMessages 115 | """.stripMargin 116 | else 117 | s"Found hole ${holeNameMsg}with type: ${printType(tpe)}" 118 | logLevel match 119 | case LogLevel.Info => 120 | ctx.reporter.report(new Info(message, holeTree.sourcePos)) 121 | case LogLevel.Warn => report.warning(message, holeTree.sourcePos) 122 | case LogLevel.Error => report.error(message, holeTree.sourcePos) 123 | } 124 | 125 | private def posSummary(pos: SourcePosition)(using Context): String = 126 | s"${pos.source().name()}:${pos.line + 1}:${pos.column}" 127 | 128 | object Hole: 129 | def unapply(tree: tpd.Tree)(using Context): Option[tpd.Tree] = 130 | Option.when(tree.symbol == defn.Predef_undefined)(tree) 131 | 132 | case class Binding(tpe: Type, pos: SourcePosition) 133 | -------------------------------------------------------------------------------- /src/main/scala-3/holes/TypedHolesPlugin.scala: -------------------------------------------------------------------------------- 1 | package holes 2 | import dotty.tools.dotc.plugins.PluginPhase 3 | import dotty.tools.dotc.plugins.StandardPlugin 4 | 5 | class TypedHolesPlugin extends StandardPlugin: 6 | val name: String = "typed-holes" 7 | override def description: String = 8 | "Treat use of ??? as a hole and give a useful warning about it" 9 | 10 | override def init(options: List[String]): List[PluginPhase] = 11 | val logLevel = 12 | options 13 | .flatMap: option => 14 | if option.startsWith("log-level:") then 15 | option.substring("log-level:".length).toLowerCase match 16 | case "info" => Some(LogLevel.Info) 17 | case "warn" => Some(LogLevel.Warn) 18 | case "error" => Some(LogLevel.Error) 19 | case _ => None 20 | else None 21 | .headOption 22 | .getOrElse(LogLevel.Warn) 23 | 24 | List( 25 | new NamedHolesPhase, 26 | TypedHolesPhase(logLevel) 27 | ) 28 | -------------------------------------------------------------------------------- /src/main/scala/holes/HoleName.scala: -------------------------------------------------------------------------------- 1 | package holes 2 | 3 | case class HoleName(name: String) 4 | -------------------------------------------------------------------------------- /src/main/scala/holes/LogLevel.scala: -------------------------------------------------------------------------------- 1 | package holes 2 | 3 | sealed abstract class LogLevel 4 | object LogLevel { 5 | case object Info extends LogLevel 6 | case object Warn extends LogLevel 7 | case object Error extends LogLevel 8 | } 9 | -------------------------------------------------------------------------------- /src/test/resources/arguments/expected-3.3.txt: -------------------------------------------------------------------------------- 1 | -- Info: src/test/resources/arguments/input.scala:9:4 -------------------------- 2 | 9 | f(???, if(1 == 2) ??? else ???) 3 | | ^^^ 4 | | Found hole with type: Int 5 | -- Info: src/test/resources/arguments/input.scala:9:20 ------------------------- 6 | 9 | f(???, if(1 == 2) ??? else ???) 7 | | ^^^ 8 | | Found hole with type: String 9 | -- Info: src/test/resources/arguments/input.scala:9:29 ------------------------- 10 | 9 | f(???, if(1 == 2) ??? else ???) 11 | | ^^^ 12 | | Found hole with type: String 13 | -- Info: src/test/resources/arguments/input.scala:12:8 ------------------------- 14 | 12 | ff(1)(???){ ??? } 15 | | ^^^ 16 | | Found hole with type: String 17 | -- Info: src/test/resources/arguments/input.scala:12:14 ------------------------ 18 | 12 | ff(1)(???){ ??? } 19 | | ^^^ 20 | | Found hole with type: a.O 21 | -- Info: src/test/resources/arguments/input.scala:16:4 ------------------------- 22 | 16 | m(???) 23 | | ^^^ 24 | | 25 | | Found hole with type: Int 26 | | Relevant bindings include 27 | | evidence$1: a.A.OpaqueInt (bound at input.scala:16:4) 28 | | 29 | -------------------------------------------------------------------------------- /src/test/resources/arguments/expected-3.txt: -------------------------------------------------------------------------------- 1 | -- Info: src/test/resources/arguments/input.scala:9:4 -------------------------- 2 | 9 | f(???, if(1 == 2) ??? else ???) 3 | | ^^^ 4 | | Found hole with type: Int 5 | -- Info: src/test/resources/arguments/input.scala:9:20 ------------------------- 6 | 9 | f(???, if(1 == 2) ??? else ???) 7 | | ^^^ 8 | | Found hole with type: String 9 | -- Info: src/test/resources/arguments/input.scala:9:29 ------------------------- 10 | 9 | f(???, if(1 == 2) ??? else ???) 11 | | ^^^ 12 | | Found hole with type: String 13 | -- Info: src/test/resources/arguments/input.scala:12:8 ------------------------- 14 | 12 | ff(1)(???){ ??? } 15 | | ^^^ 16 | | Found hole with type: String 17 | -- Info: src/test/resources/arguments/input.scala:12:14 ------------------------ 18 | 12 | ff(1)(???){ ??? } 19 | | ^^^ 20 | | Found hole with type: a.O 21 | -- Info: src/test/resources/arguments/input.scala:16:4 ------------------------- 22 | 16 | m(???) 23 | | ^^^ 24 | | 25 | | Found hole with type: Int 26 | | Relevant bindings include 27 | | contextual$1: a.A.OpaqueInt (bound at input.scala:16:4) 28 | | 29 | -------------------------------------------------------------------------------- /src/test/resources/arguments/input.scala: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | object A: 4 | opaque type OpaqueInt = Int 5 | given OpaqueInt = 1 6 | 7 | object O: 8 | def f(i: Int, s: String) = i 9 | f(???, if(1 == 2) ??? else ???) 10 | 11 | def ff(i: Int)(s: String)(u: O.type) = 1 12 | ff(1)(???){ ??? } 13 | 14 | import A.* 15 | def m(j: OpaqueInt ?=> Int): Int = j 16 | m(???) 17 | 18 | -------------------------------------------------------------------------------- /src/test/resources/if-else/expected-3.txt: -------------------------------------------------------------------------------- 1 | -- Info: src/test/resources/if-else/input.scala:7:6 ---------------------------- 2 | 7 | ??? 3 | | ^^^ 4 | | 5 | | Found hole with type: Boolean 6 | | Relevant bindings include 7 | | x: Int (bound at input.scala:5:8) 8 | | y: String (bound at input.scala:5:16) 9 | | 10 | -- Info: src/test/resources/if-else/input.scala:10:6 --------------------------- 11 | 10 | ??? 12 | | ^^^ 13 | | 14 | | Found hole with type: Boolean 15 | | Relevant bindings include 16 | | x: Int (bound at input.scala:5:8) 17 | | y: String (bound at input.scala:5:16) 18 | | 19 | -- Info: src/test/resources/if-else/input.scala:15:23 -------------------------- 20 | 15 | if (y.length == x) ??? 21 | | ^^^ 22 | | 23 | | Found hole with type: Boolean 24 | | Relevant bindings include 25 | | x: Int (bound at input.scala:14:8) 26 | | y: String (bound at input.scala:14:16) 27 | | 28 | -------------------------------------------------------------------------------- /src/test/resources/if-else/expected.txt: -------------------------------------------------------------------------------- 1 | src/test/resources/if-else/input.scala:7: 2 | Found hole with type: Boolean 3 | Relevant bindings include 4 | x: Int (bound at input.scala:5:9) 5 | y: String (bound at input.scala:5:17) 6 | 7 | ??? 8 | ^ 9 | src/test/resources/if-else/input.scala:10: 10 | Found hole with type: Boolean 11 | Relevant bindings include 12 | x: Int (bound at input.scala:5:9) 13 | y: String (bound at input.scala:5:17) 14 | 15 | ??? 16 | ^ 17 | src/test/resources/if-else/input.scala:15: 18 | Found hole with type: Boolean 19 | Relevant bindings include 20 | x: Int (bound at input.scala:14:9) 21 | y: String (bound at input.scala:14:17) 22 | 23 | if (y.length == x) ??? 24 | ^ -------------------------------------------------------------------------------- /src/test/resources/if-else/input.scala: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | object Foo { 4 | 5 | def a(x: Int, y: String): Boolean = { 6 | if (y.length == x) { 7 | ??? 8 | } else { 9 | val umm = "123" 10 | ??? 11 | } 12 | } 13 | 14 | def b(x: Int, y: String): Boolean = 15 | if (y.length == x) ??? 16 | else true 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/test/resources/named-args/expected-3.txt: -------------------------------------------------------------------------------- 1 | -- Info: src/test/resources/named-args/input.scala:5:29 ------------------------ 2 | 5 | def bar = foo(y = ???, x = ???) 3 | | ^ 4 | | Found hole with type: Int 5 | -- Info: src/test/resources/named-args/input.scala:5:20 ------------------------ 6 | 5 | def bar = foo(y = ???, x = ???) 7 | | ^ 8 | | Found hole with type: String -------------------------------------------------------------------------------- /src/test/resources/named-args/input.scala: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | object Foo { 4 | def foo(x: Int, y: String): Boolean = true 5 | def bar = foo(y = ???, x = ???) 6 | } 7 | -------------------------------------------------------------------------------- /src/test/resources/named-holes/expected-3.txt: -------------------------------------------------------------------------------- 1 | -- Info: src/test/resources/named-holes/input.scala:12:25 ---------------------- 2 | 12 | case Left(error) => __left 3 | | ^^^^^^ 4 | | 5 | | Found hole 'left' with type: Option[Result] 6 | | Relevant bindings include 7 | | args: Array[String] (bound at input.scala:11:12) 8 | | 9 | -- Info: src/test/resources/named-holes/input.scala:13:25 ---------------------- 10 | 13 | case Right(x) => __right 11 | | ^^^^^^^ 12 | | 13 | | Found hole 'right' with type: Option[Result] 14 | | Relevant bindings include 15 | | args: Array[String] (bound at input.scala:11:12) 16 | | 17 | -------------------------------------------------------------------------------- /src/test/resources/named-holes/expected.txt: -------------------------------------------------------------------------------- 1 | src/test/resources/named-holes/input.scala:12: 2 | Found hole 'left' with type: Option[Result] 3 | Relevant bindings include 4 | args: Array[String] (bound at input.scala:11:13) 5 | error: String (bound at input.scala:12:15) 6 | 7 | case Left(error) => __left 8 | ^ 9 | src/test/resources/named-holes/input.scala:13: 10 | Found hole 'right' with type: Option[Result] 11 | Relevant bindings include 12 | args: Array[String] (bound at input.scala:11:13) 13 | x: Int (bound at input.scala:13:16) 14 | 15 | case Right(x) => __right 16 | ^ 17 | -------------------------------------------------------------------------------- /src/test/resources/named-holes/input.scala: -------------------------------------------------------------------------------- 1 | case class Result() 2 | 3 | object Foo { 4 | 5 | def doStuff(args: Array[String]): Either[String, Int] = Left("nope!") 6 | 7 | } 8 | 9 | object Bar { 10 | 11 | def hello(args: Array[String]): Option[Result] = Foo.doStuff(args) match { 12 | case Left(error) => __left 13 | case Right(x) => __right 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /src/test/resources/nested-defs/expected-3.txt: -------------------------------------------------------------------------------- 1 | -- Info: src/test/resources/nested-defs/input.scala:8:6 ------------------------ 2 | 8 | ??? 3 | | ^^^ 4 | | 5 | | Found hole with type: Double 6 | | Relevant bindings include 7 | | x: Int (bound at input.scala:7:15) 8 | | y: String (bound at input.scala:5:21) 9 | | z: Boolean (bound at input.scala:7:23) 10 | | 11 | -- Info: src/test/resources/nested-defs/input.scala:13:8 ----------------------- 12 | 13 | ??? 13 | | ^^^ 14 | | 15 | | Found hole with type: String 16 | | Relevant bindings include 17 | | x: Int (bound at input.scala:11:15) 18 | | y: String (bound at input.scala:5:21) 19 | | 20 | -- Info: src/test/resources/nested-defs/input.scala:16:8 ----------------------- 21 | 16 | ??? 22 | | ^^^ 23 | | 24 | | Found hole with type: String 25 | | Relevant bindings include 26 | | x: Int (bound at input.scala:11:15) 27 | | y: String (bound at input.scala:5:21) 28 | | 29 | -------------------------------------------------------------------------------- /src/test/resources/nested-defs/expected.txt: -------------------------------------------------------------------------------- 1 | src/test/resources/nested-defs/input.scala:8: 2 | Found hole with type: Double 3 | Relevant bindings include 4 | x: Int (bound at input.scala:7:16) 5 | y: String (bound at input.scala:5:22) 6 | z: Boolean (bound at input.scala:7:24) 7 | 8 | ??? 9 | ^ 10 | src/test/resources/nested-defs/input.scala:13: 11 | Found hole with type: String 12 | Relevant bindings include 13 | x: Int (bound at input.scala:11:16) 14 | y: String (bound at input.scala:5:22) 15 | 16 | ??? 17 | ^ 18 | src/test/resources/nested-defs/input.scala:16: 19 | Found hole with type: String 20 | Relevant bindings include 21 | x: Int (bound at input.scala:11:16) 22 | y: String (bound at input.scala:5:22) 23 | 24 | ??? 25 | ^ -------------------------------------------------------------------------------- /src/test/resources/nested-defs/input.scala: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | object Foo { 4 | 5 | def parent(x: Int, y: String): Boolean = { 6 | 7 | def child1(x: Int, z: Boolean): Double = { 8 | ??? 9 | } 10 | 11 | def child2(x: Int): String = { 12 | if (y.length == x) { 13 | ??? 14 | } else { 15 | val umm = "123" 16 | ??? 17 | } 18 | } 19 | 20 | child1(123, true) == 0.0 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/test/resources/pattern-match-on-either/expected-3.txt: -------------------------------------------------------------------------------- 1 | -- Info: src/test/resources/pattern-match-on-either/input.scala:10:21 ---------- 2 | 10 | case Left(_) => ??? 3 | | ^^^ 4 | | 5 | | Found hole with type: Option[Ops] 6 | | Relevant bindings include 7 | | args: List[String] (bound at input.scala:9:19) 8 | | 9 | -- Info: src/test/resources/pattern-match-on-either/input.scala:11:21 ---------- 10 | 11 | case Right(_) => ??? 11 | | ^^^ 12 | | 13 | | Found hole with type: Option[Ops] 14 | | Relevant bindings include 15 | | args: List[String] (bound at input.scala:9:19) 16 | | 17 | -------------------------------------------------------------------------------- /src/test/resources/pattern-match-on-either/expected.txt: -------------------------------------------------------------------------------- 1 | src/test/resources/pattern-match-on-either/input.scala:10: 2 | Found hole with type: Option[Ops] 3 | Relevant bindings include 4 | args: List[String] (bound at input.scala:9:20) 5 | 6 | case Left(_) => ??? 7 | ^ 8 | src/test/resources/pattern-match-on-either/input.scala:11: 9 | Found hole with type: Option[Ops] 10 | Relevant bindings include 11 | args: List[String] (bound at input.scala:9:20) 12 | 13 | case Right(_) => ??? 14 | ^ -------------------------------------------------------------------------------- /src/test/resources/pattern-match-on-either/input.scala: -------------------------------------------------------------------------------- 1 | object CaseApp { 2 | def parse[A](args: Seq[String]): Either[String, A] = Left("nope!") 3 | } 4 | 5 | case class Ops() 6 | 7 | object Main { 8 | 9 | def parseCmdLine(args: List[String]): Option[Ops] = CaseApp.parse[Ops](args.toSeq) match { 10 | case Left(_) => ??? 11 | case Right(_) => ??? 12 | } 13 | 14 | } -------------------------------------------------------------------------------- /src/test/resources/pattern-match-on-nested-case-class/expected-3.txt: -------------------------------------------------------------------------------- 1 | -- Info: src/test/resources/pattern-match-on-nested-case-class/input.scala:9:76 2 | 9 | case p @ Parent(wow, yeah, c @ Child(hmm, ok, _), d, Child(_, _, _)) => ??? 3 | | ^^^ 4 | | 5 | | Found hole with type: Boolean 6 | | Relevant bindings include 7 | | parent: foo.Parent (bound at input.scala:8:10) 8 | | 9 | -------------------------------------------------------------------------------- /src/test/resources/pattern-match-on-nested-case-class/expected.txt: -------------------------------------------------------------------------------- 1 | src/test/resources/pattern-match-on-nested-case-class/input.scala:9: 2 | Found hole with type: Boolean 3 | Relevant bindings include 4 | c: foo.Child (bound at input.scala:9:32) 5 | d: foo.Child (bound at input.scala:9:55) 6 | hmm: Int (bound at input.scala:9:42) 7 | ok: String (bound at input.scala:9:47) 8 | p: foo.Parent (bound at input.scala:9:10) 9 | parent: foo.Parent (bound at input.scala:8:11) 10 | wow: String (bound at input.scala:9:21) 11 | yeah: Int (bound at input.scala:9:26) 12 | 13 | case p @ Parent(wow, yeah, c @ Child(hmm, ok, _), d, Child(_, _, _)) => ??? 14 | ^ -------------------------------------------------------------------------------- /src/test/resources/pattern-match-on-nested-case-class/input.scala: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | case class Child(x: Int, y: String, z: Boolean) 4 | case class Parent(a: String, b: Int, c: Child, d: Child, e: Child) 5 | 6 | object Foo { 7 | 8 | def bar(parent: Parent): Boolean = parent match { 9 | case p @ Parent(wow, yeah, c @ Child(hmm, ok, _), d, Child(_, _, _)) => ??? 10 | } 11 | } -------------------------------------------------------------------------------- /src/test/resources/pattern-match/expected-3.txt: -------------------------------------------------------------------------------- 1 | -- Info: src/test/resources/pattern-match/input.scala:9:6 ---------------------- 2 | 9 | ??? 3 | | ^^^ 4 | | 5 | | Found hole with type: String 6 | | Relevant bindings include 7 | | x: Int (bound at input.scala:5:10) 8 | | 9 | -- Info: src/test/resources/pattern-match/input.scala:10:14 -------------------- 10 | 10 | case _ => ??? 11 | | ^^^ 12 | | 13 | | Found hole with type: String 14 | | Relevant bindings include 15 | | x: Int (bound at input.scala:5:10) 16 | | 17 | -------------------------------------------------------------------------------- /src/test/resources/pattern-match/expected.txt: -------------------------------------------------------------------------------- 1 | src/test/resources/pattern-match/input.scala:9: 2 | Found hole with type: String 3 | Relevant bindings include 4 | x: Int (bound at input.scala:5:11) 5 | 6 | ??? 7 | ^ 8 | src/test/resources/pattern-match/input.scala:10: 9 | Found hole with type: String 10 | Relevant bindings include 11 | x: Int (bound at input.scala:5:11) 12 | 13 | case _ => ??? 14 | ^ -------------------------------------------------------------------------------- /src/test/resources/pattern-match/input.scala: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | object Foo { 4 | 5 | def bar(x: Int): String = x match { 6 | case 0 => "zero" 7 | case 1 => 8 | val dunno = "one?" 9 | ??? 10 | case _ => ??? 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/test/resources/polymorphic/expected-3.txt: -------------------------------------------------------------------------------- 1 | -- Info: src/test/resources/polymorphic/input.scala:7:9 ------------------------ 2 | 7 | else ??? 3 | | ^^^ 4 | | 5 | | Found hole with type: String 6 | | Relevant bindings include 7 | | x: A (bound at input.scala:5:21) 8 | | y: A (bound at input.scala:5:27) 9 | | 10 | -------------------------------------------------------------------------------- /src/test/resources/polymorphic/expected.txt: -------------------------------------------------------------------------------- 1 | src/test/resources/polymorphic/input.scala:7: 2 | Found hole with type: String 3 | Relevant bindings include 4 | x: A (bound at input.scala:5:22) 5 | y: A (bound at input.scala:5:28) 6 | 7 | else ??? 8 | ^ -------------------------------------------------------------------------------- /src/test/resources/polymorphic/input.scala: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | object Foo { 4 | 5 | def polymorphic[A](x: A, y: A): String = { 6 | if (x == y) "equal" 7 | else ??? 8 | } 9 | 10 | } -------------------------------------------------------------------------------- /src/test/resources/simple/expected-3.txt: -------------------------------------------------------------------------------- 1 | -- Info: src/test/resources/simple/input.scala:4:15 ---------------------------- 2 | 4 | val i: Int = ??? 3 | | ^^^ 4 | | Found hole with type: Int 5 | -------------------------------------------------------------------------------- /src/test/resources/simple/expected.txt: -------------------------------------------------------------------------------- 1 | src/test/resources/simple/input.scala:4: Found hole with type: Int 2 | val i: Int = ??? 3 | ^ 4 | -------------------------------------------------------------------------------- /src/test/resources/simple/input.scala: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | object O { 4 | val i: Int = ??? 5 | } 6 | -------------------------------------------------------------------------------- /src/test/resources/type-ascription/expected-3.txt: -------------------------------------------------------------------------------- 1 | -- Info: src/test/resources/type-ascription/input.scala:5:13 ------------------- 2 | 5 | val a = (__ahole: Short) + i 3 | | ^^^^^^^ 4 | | 5 | | Found hole 'ahole' with type: Short 6 | | Relevant bindings include 7 | | i: Double (bound at input.scala:4:35) 8 | | -------------------------------------------------------------------------------- /src/test/resources/type-ascription/input.scala: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | object Foo: 4 | def aFunctionWithATypedTypedHole(i: Double): Double = 5 | val a = (__ahole: Short) + i 6 | a + 1.0 -------------------------------------------------------------------------------- /src/test/resources/val-var-def-one-liners/expected-3.txt: -------------------------------------------------------------------------------- 1 | -- Info: src/test/resources/val-var-def-one-liners/input.scala:5:14 ------------ 2 | 5 | def hole1 = ??? 3 | | ^^^ 4 | | Found hole with type: Nothing 5 | -- Info: src/test/resources/val-var-def-one-liners/input.scala:7:28 ------------ 6 | 7 | def hole2: List[String] = ??? 7 | | ^^^ 8 | | Found hole with type: List[String] 9 | -- Info: src/test/resources/val-var-def-one-liners/input.scala:9:14 ------------ 10 | 9 | val hole3 = ??? 11 | | ^^^ 12 | | Found hole with type: Nothing 13 | -- Info: src/test/resources/val-var-def-one-liners/input.scala:11:25 ----------- 14 | 11 | val hole4: Option[_] = ??? 15 | | ^^^ 16 | | Found hole with type: Option[?] 17 | -- Info: src/test/resources/val-var-def-one-liners/input.scala:13:14 ----------- 18 | 13 | var hole5 = ??? 19 | | ^^^ 20 | | Found hole with type: Nothing 21 | -- Info: src/test/resources/val-var-def-one-liners/input.scala:15:30 ----------- 22 | 15 | var hole6: Option[String] = ??? 23 | | ^^^ 24 | | Found hole with type: Option[String] 25 | -- Info: src/test/resources/val-var-def-one-liners/input.scala:17:16 ----------- 26 | 17 | val hole7 = { ??? } 27 | | ^^^ 28 | | Found hole with type: Nothing 29 | -- Info: src/test/resources/val-var-def-one-liners/input.scala:19:27 ----------- 30 | 19 | val hole8: () => Int = { ??? } 31 | | ^^^ 32 | | Found hole with type: () => Int 33 | -- Info: src/test/resources/val-var-def-one-liners/input.scala:21:31 ----------- 34 | 21 | val hole9 = { (x: String) => ??? } 35 | | ^^^ 36 | | 37 | | Found hole with type: Nothing 38 | | Relevant bindings include 39 | | x: String (bound at input.scala:21:17) 40 | | 41 | -- Info: src/test/resources/val-var-def-one-liners/input.scala:23:37 ----------- 42 | 23 | val hole10: String => Int = { x => ??? } 43 | | ^^^ 44 | | 45 | | Found hole with type: Int 46 | | Relevant bindings include 47 | | x: String (bound at input.scala:23:32) 48 | | 49 | -- Info: src/test/resources/val-var-def-one-liners/input.scala:25:40 ----------- 50 | 25 | val hole11 = { (a: Int, b: String) => ??? } 51 | | ^^^ 52 | | 53 | | Found hole with type: Nothing 54 | | Relevant bindings include 55 | | a: Int (bound at input.scala:25:18) 56 | | b: String (bound at input.scala:25:26) 57 | | 58 | -- Info: src/test/resources/val-var-def-one-liners/input.scala:27:49 ----------- 59 | 27 | val hole12: (Int, String) => Int = { (a, b) => ??? } 60 | | ^^^ 61 | | 62 | | Found hole with type: Int 63 | | Relevant bindings include 64 | | a: Int (bound at input.scala:27:40) 65 | | b: String (bound at input.scala:27:43) 66 | | 67 | -- Info: src/test/resources/val-var-def-one-liners/input.scala:29:59 ----------- 68 | 29 | val hole13: (Int, String) => (String, Int) = { (a, b) => ??? } 69 | | ^^^ 70 | | 71 | | Found hole with type: (String, Int) 72 | | Relevant bindings include 73 | | a: Int (bound at input.scala:29:50) 74 | | b: String (bound at input.scala:29:53) 75 | | 76 | -- Info: src/test/resources/val-var-def-one-liners/input.scala:31:63 ----------- 77 | 31 | val hole14: String => Int = { s => s.foldLeft(0) { (a, c) => ??? } } 78 | | ^^^ 79 | | 80 | | Found hole with type: Int 81 | | Relevant bindings include 82 | | a: Int (bound at input.scala:31:54) 83 | | c: Char (bound at input.scala:31:57) 84 | | s: String (bound at input.scala:31:32) 85 | | 86 | -------------------------------------------------------------------------------- /src/test/resources/val-var-def-one-liners/expected.txt: -------------------------------------------------------------------------------- 1 | src/test/resources/val-var-def-one-liners/input.scala:5: Found hole with type: Nothing 2 | def hole1 = ??? 3 | ^ 4 | src/test/resources/val-var-def-one-liners/input.scala:7: Found hole with type: List[String] 5 | def hole2: List[String] = ??? 6 | ^ 7 | src/test/resources/val-var-def-one-liners/input.scala:9: Found hole with type: Nothing 8 | val hole3 = ??? 9 | ^ 10 | src/test/resources/val-var-def-one-liners/input.scala:11: Found hole with type: Option[_] 11 | val hole4: Option[_] = ??? 12 | ^ 13 | src/test/resources/val-var-def-one-liners/input.scala:13: Found hole with type: Nothing 14 | var hole5 = ??? 15 | ^ 16 | src/test/resources/val-var-def-one-liners/input.scala:15: Found hole with type: Option[String] 17 | var hole6: Option[String] = ??? 18 | ^ 19 | src/test/resources/val-var-def-one-liners/input.scala:17: Found hole with type: Nothing 20 | val hole7 = { ??? } 21 | ^ 22 | src/test/resources/val-var-def-one-liners/input.scala:19: Found hole with type: () => Int 23 | val hole8: () => Int = { ??? } 24 | ^ 25 | src/test/resources/val-var-def-one-liners/input.scala:21: 26 | Found hole with type: Nothing 27 | Relevant bindings include 28 | x: String (bound at input.scala:21:19) 29 | 30 | val hole9 = { (x: String) => ??? } 31 | ^ 32 | src/test/resources/val-var-def-one-liners/input.scala:23: 33 | Found hole with type: Int 34 | Relevant bindings include 35 | x: String (bound at input.scala:23:33) 36 | 37 | val hole10: String => Int = { x => ??? } 38 | ^ 39 | src/test/resources/val-var-def-one-liners/input.scala:25: 40 | Found hole with type: Nothing 41 | Relevant bindings include 42 | a: Int (bound at input.scala:25:20) 43 | b: String (bound at input.scala:25:28) 44 | 45 | val hole11 = { (a: Int, b: String) => ??? } 46 | ^ 47 | src/test/resources/val-var-def-one-liners/input.scala:27: 48 | Found hole with type: Int 49 | Relevant bindings include 50 | a: Int (bound at input.scala:27:41) 51 | b: String (bound at input.scala:27:44) 52 | 53 | val hole12: (Int, String) => Int = { (a, b) => ??? } 54 | ^ 55 | src/test/resources/val-var-def-one-liners/input.scala:29: 56 | Found hole with type: (String, Int) 57 | Relevant bindings include 58 | a: Int (bound at input.scala:29:51) 59 | b: String (bound at input.scala:29:54) 60 | 61 | val hole13: (Int, String) => (String, Int) = { (a, b) => ??? } 62 | ^ 63 | src/test/resources/val-var-def-one-liners/input.scala:31: 64 | Found hole with type: Int 65 | Relevant bindings include 66 | a: Int (bound at input.scala:31:55) 67 | c: Char (bound at input.scala:31:58) 68 | s: String (bound at input.scala:31:33) 69 | 70 | val hole14: String => Int = { s => s.foldLeft(0) { (a, c) => ??? } } 71 | ^ 72 | -------------------------------------------------------------------------------- /src/test/resources/val-var-def-one-liners/input.scala: -------------------------------------------------------------------------------- 1 | package foo 2 | 3 | object Foo { 4 | 5 | def hole1 = ??? 6 | 7 | def hole2: List[String] = ??? 8 | 9 | val hole3 = ??? 10 | 11 | val hole4: Option[_] = ??? 12 | 13 | var hole5 = ??? 14 | 15 | var hole6: Option[String] = ??? 16 | 17 | val hole7 = { ??? } 18 | 19 | val hole8: () => Int = { ??? } 20 | 21 | val hole9 = { (x: String) => ??? } 22 | 23 | val hole10: String => Int = { x => ??? } 24 | 25 | val hole11 = { (a: Int, b: String) => ??? } 26 | 27 | val hole12: (Int, String) => Int = { (a, b) => ??? } 28 | 29 | val hole13: (Int, String) => (String, Int) = { (a, b) => ??? } 30 | 31 | val hole14: String => Int = { s => s.foldLeft(0) { (a, c) => ??? } } 32 | 33 | } -------------------------------------------------------------------------------- /src/test/scala/holes/IntegrationTests.scala: -------------------------------------------------------------------------------- 1 | package holes 2 | 3 | import buildinfo.BuildInfo.scalaVersion 4 | import org.apache.commons.io.FileUtils 5 | import org.scalatest.BeforeAndAfterAll 6 | import org.scalatest.funspec.AnyFunSpec 7 | 8 | import java.nio.charset.StandardCharsets 9 | import java.nio.file.Files 10 | import java.nio.file.Path 11 | import java.nio.file.Paths 12 | import scala.sys.process._ 13 | 14 | class IntegrationTests extends AnyFunSpec with BeforeAndAfterAll { 15 | 16 | private val pluginJar = sys.props("plugin.jar") 17 | private val scalacClasspath = sys.props("scalac.classpath") 18 | private val targetDir = Paths.get("target/integration-tests") 19 | private def isScala3 = scalaVersion.startsWith("3") 20 | 21 | private def runScalac(args: String*): String = { 22 | val buf = new StringBuffer 23 | val logger = new ProcessLogger { 24 | override def out(s: => String): Unit = { buf.append(s); buf.append('\n') } 25 | override def err(s: => String): Unit = { buf.append(s); buf.append('\n') } 26 | override def buffer[T](f: => T): T = f 27 | } 28 | 29 | val className = 30 | if (isScala3) "dotty.tools.dotc.Main" 31 | else "scala.tools.nsc.Main" 32 | 33 | Process( 34 | "java" 35 | :: "-Dscala.usejavacp=true" 36 | :: "-cp" :: scalacClasspath 37 | :: className 38 | :: args.filter(_.nonEmpty).toList 39 | ).!(logger) 40 | 41 | buf.toString 42 | } 43 | 44 | private def compileFile(path: Path): String = 45 | runScalac( 46 | s"-Xplugin:$pluginJar", 47 | "-P:typed-holes:log-level:info", 48 | if (isScala3) "-color:never" else "", 49 | if (isScala3) "-nowarn" else "", 50 | "-d", 51 | targetDir.toString, 52 | path.toString 53 | ) 54 | 55 | override def beforeAll(): Unit = { 56 | println(runScalac("-version")) 57 | 58 | FileUtils.deleteQuietly(targetDir.toFile) 59 | Files.createDirectories(targetDir) 60 | } 61 | 62 | describe("produces the expected output") { 63 | for ( 64 | scenario <- Paths 65 | .get("src/test/resources") 66 | .toFile 67 | .listFiles() 68 | .toList 69 | .map(_.toPath) 70 | ) { 71 | expectFile(scalaVersion, scenario) match { 72 | case Some(expectedFile) => 73 | it(scenario.getFileName.toString) { 74 | val expected = 75 | new String( 76 | Files.readAllBytes(expectedFile), 77 | StandardCharsets.UTF_8 78 | ).trim 79 | val actual = 80 | compileFile(scenario.resolve("input.scala")).trim 81 | 82 | if (actual != expected) { 83 | val tmpActual = Files.createTempFile("actual", ".txt") 84 | Files.write(tmpActual, actual.getBytes(StandardCharsets.UTF_8)) 85 | println("Compiler output:") 86 | println(s"Written to: $tmpActual") 87 | println("=====") 88 | println(actual) 89 | println("=====") 90 | println("Expected output:") 91 | println("=====") 92 | println(expected) 93 | println("=====") 94 | println(s"Output written to $tmpActual") 95 | } 96 | assert(actual === expected) 97 | } 98 | case None => 99 | ignore(scenario.getFileName.toString) {} 100 | } 101 | } 102 | } 103 | 104 | private def expectFile(scalaVersion: String, scenario: Path) = { 105 | val possibleNames = scalaVersion.split('.') match { 106 | case Array("3", m, _) => 107 | List( 108 | s"expected-3.$m.txt", 109 | s"expected-3.txt" 110 | ) 111 | case Array("2", mi, _) => List(s"expected.txt") 112 | case _ => Nil 113 | } 114 | possibleNames.collectFirst { 115 | case fileName if scenario.resolve(fileName).toFile.exists() => 116 | scenario.resolve(fileName) 117 | } 118 | } 119 | } 120 | --------------------------------------------------------------------------------