├── .git-blame-ignore-revs ├── .github └── workflows │ ├── ci.yml │ └── clean.yml ├── .gitignore ├── .mergify.yml ├── .scalafmt.conf ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── build.sbt ├── plugin └── src │ └── main │ ├── resources │ ├── plugin.properties │ └── scalac-plugin.xml │ ├── scala-2 │ ├── BetterToStringPlugin.scala │ └── Scala2CompilerApi.scala │ ├── scala-3.1.x │ └── AfterPhase.scala │ ├── scala-3.2.x │ └── AfterPhase.scala │ ├── scala-3.3.x │ └── AfterPhase.scala │ ├── scala-3.4.x │ └── AfterPhase.scala │ ├── scala-3.5.x │ └── AfterPhase.scala │ ├── scala-3.6.x │ └── AfterPhase.scala │ ├── scala-3.7.x │ └── AfterPhase.scala │ ├── scala-3 │ ├── BetterToStringPlugin.scala │ └── Scala3CompilerApi.scala │ └── scala │ └── BetterToStringImpl.scala ├── project ├── BackpublishPlugin.scala ├── ReadmePlugin.scala ├── build.properties └── plugins.sbt ├── scala-versions └── tests └── src └── test ├── scala-3 └── Scala3Tests.scala └── scala ├── Tests.scala └── pack └── package.scala /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # Scala Steward: Reformat with scalafmt 3.8.6 2 | 41e1a8621e83f0fa7a16add34c967c0218a58fc4 3 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by sbt-github-actions using the 2 | # githubWorkflowGenerate task. You should add and commit this file to 3 | # your git repository. It goes without saying that you shouldn't edit 4 | # this file by hand! Instead, if you wish to make changes, you should 5 | # change your sbt build configuration to revise the workflow description 6 | # to meet your needs, then regenerate this file. 7 | 8 | name: Continuous Integration 9 | 10 | on: 11 | pull_request: 12 | branches: ['**', '!update/**', '!pr/**'] 13 | push: 14 | branches: ['**', '!update/**', '!pr/**'] 15 | tags: [v*] 16 | 17 | env: 18 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 19 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 20 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 21 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | 24 | 25 | concurrency: 26 | group: ${{ github.workflow }} @ ${{ github.ref }} 27 | cancel-in-progress: true 28 | 29 | jobs: 30 | build: 31 | name: Test 32 | strategy: 33 | matrix: 34 | os: [ubuntu-22.04] 35 | scala: 36 | - 2.12.18 37 | - 2.12.19 38 | - 2.12.20 39 | - 2.13.12 40 | - 2.13.13 41 | - 2.13.14 42 | - 2.13.15 43 | - 2.13.16 44 | - 3.3.0 45 | - 3.3.1 46 | - 3.3.3 47 | - 3.3.4 48 | - 3.3.5 49 | - 3.3.6 50 | - 3.4.0 51 | - 3.4.1 52 | - 3.4.2 53 | - 3.4.3 54 | - 3.5.0 55 | - 3.5.1 56 | - 3.5.2 57 | - 3.6.2 58 | - 3.6.3 59 | - 3.6.4 60 | - 3.7.0 61 | - 3.7.1 62 | java: [temurin@8] 63 | runs-on: ${{ matrix.os }} 64 | timeout-minutes: 60 65 | steps: 66 | - name: Checkout current branch (full) 67 | uses: actions/checkout@v4 68 | with: 69 | fetch-depth: 0 70 | 71 | - name: Setup sbt 72 | uses: sbt/setup-sbt@v1 73 | 74 | - name: Setup Java (temurin@8) 75 | id: setup-java-temurin-8 76 | if: matrix.java == 'temurin@8' 77 | uses: actions/setup-java@v4 78 | with: 79 | distribution: temurin 80 | java-version: 8 81 | cache: sbt 82 | 83 | - name: sbt update 84 | if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' 85 | run: sbt +update 86 | 87 | - name: Check that workflows are up to date 88 | run: sbt githubWorkflowCheck 89 | 90 | - name: Check headers and formatting 91 | if: matrix.java == 'temurin@8' && matrix.os == 'ubuntu-22.04' 92 | run: sbt '++ ${{ matrix.scala }}' headerCheckAll scalafmtCheckAll 'project /' scalafmtSbtCheck 93 | 94 | - name: Test 95 | run: sbt '++ ${{ matrix.scala }}' test 96 | 97 | - name: Check binary compatibility 98 | if: matrix.java == 'temurin@8' && matrix.os == 'ubuntu-22.04' 99 | run: sbt '++ ${{ matrix.scala }}' mimaReportBinaryIssues 100 | 101 | - name: Generate API documentation 102 | if: matrix.java == 'temurin@8' && matrix.os == 'ubuntu-22.04' 103 | run: sbt '++ ${{ matrix.scala }}' doc 104 | 105 | - name: Make target directories 106 | if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) 107 | run: mkdir -p plugin/target project/target 108 | 109 | - name: Compress target directories 110 | if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) 111 | run: tar cf targets.tar plugin/target project/target 112 | 113 | - name: Upload target directories 114 | if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) 115 | uses: actions/upload-artifact@v4 116 | with: 117 | name: target-${{ matrix.os }}-${{ matrix.java }}-${{ matrix.scala }} 118 | path: targets.tar 119 | 120 | publish: 121 | name: Publish Artifacts 122 | needs: [build] 123 | if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v')) 124 | strategy: 125 | matrix: 126 | os: [ubuntu-22.04] 127 | java: [temurin@8] 128 | runs-on: ${{ matrix.os }} 129 | steps: 130 | - name: Checkout current branch (full) 131 | uses: actions/checkout@v4 132 | with: 133 | fetch-depth: 0 134 | 135 | - name: Setup sbt 136 | uses: sbt/setup-sbt@v1 137 | 138 | - name: Setup Java (temurin@8) 139 | id: setup-java-temurin-8 140 | if: matrix.java == 'temurin@8' 141 | uses: actions/setup-java@v4 142 | with: 143 | distribution: temurin 144 | java-version: 8 145 | cache: sbt 146 | 147 | - name: sbt update 148 | if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' 149 | run: sbt +update 150 | 151 | - name: Download target directories (2.12.18) 152 | uses: actions/download-artifact@v4 153 | with: 154 | name: target-${{ matrix.os }}-${{ matrix.java }}-2.12.18 155 | 156 | - name: Inflate target directories (2.12.18) 157 | run: | 158 | tar xf targets.tar 159 | rm targets.tar 160 | 161 | - name: Download target directories (2.12.19) 162 | uses: actions/download-artifact@v4 163 | with: 164 | name: target-${{ matrix.os }}-${{ matrix.java }}-2.12.19 165 | 166 | - name: Inflate target directories (2.12.19) 167 | run: | 168 | tar xf targets.tar 169 | rm targets.tar 170 | 171 | - name: Download target directories (2.12.20) 172 | uses: actions/download-artifact@v4 173 | with: 174 | name: target-${{ matrix.os }}-${{ matrix.java }}-2.12.20 175 | 176 | - name: Inflate target directories (2.12.20) 177 | run: | 178 | tar xf targets.tar 179 | rm targets.tar 180 | 181 | - name: Download target directories (2.13.12) 182 | uses: actions/download-artifact@v4 183 | with: 184 | name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.12 185 | 186 | - name: Inflate target directories (2.13.12) 187 | run: | 188 | tar xf targets.tar 189 | rm targets.tar 190 | 191 | - name: Download target directories (2.13.13) 192 | uses: actions/download-artifact@v4 193 | with: 194 | name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.13 195 | 196 | - name: Inflate target directories (2.13.13) 197 | run: | 198 | tar xf targets.tar 199 | rm targets.tar 200 | 201 | - name: Download target directories (2.13.14) 202 | uses: actions/download-artifact@v4 203 | with: 204 | name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.14 205 | 206 | - name: Inflate target directories (2.13.14) 207 | run: | 208 | tar xf targets.tar 209 | rm targets.tar 210 | 211 | - name: Download target directories (2.13.15) 212 | uses: actions/download-artifact@v4 213 | with: 214 | name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.15 215 | 216 | - name: Inflate target directories (2.13.15) 217 | run: | 218 | tar xf targets.tar 219 | rm targets.tar 220 | 221 | - name: Download target directories (2.13.16) 222 | uses: actions/download-artifact@v4 223 | with: 224 | name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.16 225 | 226 | - name: Inflate target directories (2.13.16) 227 | run: | 228 | tar xf targets.tar 229 | rm targets.tar 230 | 231 | - name: Download target directories (3.3.0) 232 | uses: actions/download-artifact@v4 233 | with: 234 | name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.0 235 | 236 | - name: Inflate target directories (3.3.0) 237 | run: | 238 | tar xf targets.tar 239 | rm targets.tar 240 | 241 | - name: Download target directories (3.3.1) 242 | uses: actions/download-artifact@v4 243 | with: 244 | name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.1 245 | 246 | - name: Inflate target directories (3.3.1) 247 | run: | 248 | tar xf targets.tar 249 | rm targets.tar 250 | 251 | - name: Download target directories (3.3.3) 252 | uses: actions/download-artifact@v4 253 | with: 254 | name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.3 255 | 256 | - name: Inflate target directories (3.3.3) 257 | run: | 258 | tar xf targets.tar 259 | rm targets.tar 260 | 261 | - name: Download target directories (3.3.4) 262 | uses: actions/download-artifact@v4 263 | with: 264 | name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.4 265 | 266 | - name: Inflate target directories (3.3.4) 267 | run: | 268 | tar xf targets.tar 269 | rm targets.tar 270 | 271 | - name: Download target directories (3.3.5) 272 | uses: actions/download-artifact@v4 273 | with: 274 | name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.5 275 | 276 | - name: Inflate target directories (3.3.5) 277 | run: | 278 | tar xf targets.tar 279 | rm targets.tar 280 | 281 | - name: Download target directories (3.3.6) 282 | uses: actions/download-artifact@v4 283 | with: 284 | name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.6 285 | 286 | - name: Inflate target directories (3.3.6) 287 | run: | 288 | tar xf targets.tar 289 | rm targets.tar 290 | 291 | - name: Download target directories (3.4.0) 292 | uses: actions/download-artifact@v4 293 | with: 294 | name: target-${{ matrix.os }}-${{ matrix.java }}-3.4.0 295 | 296 | - name: Inflate target directories (3.4.0) 297 | run: | 298 | tar xf targets.tar 299 | rm targets.tar 300 | 301 | - name: Download target directories (3.4.1) 302 | uses: actions/download-artifact@v4 303 | with: 304 | name: target-${{ matrix.os }}-${{ matrix.java }}-3.4.1 305 | 306 | - name: Inflate target directories (3.4.1) 307 | run: | 308 | tar xf targets.tar 309 | rm targets.tar 310 | 311 | - name: Download target directories (3.4.2) 312 | uses: actions/download-artifact@v4 313 | with: 314 | name: target-${{ matrix.os }}-${{ matrix.java }}-3.4.2 315 | 316 | - name: Inflate target directories (3.4.2) 317 | run: | 318 | tar xf targets.tar 319 | rm targets.tar 320 | 321 | - name: Download target directories (3.4.3) 322 | uses: actions/download-artifact@v4 323 | with: 324 | name: target-${{ matrix.os }}-${{ matrix.java }}-3.4.3 325 | 326 | - name: Inflate target directories (3.4.3) 327 | run: | 328 | tar xf targets.tar 329 | rm targets.tar 330 | 331 | - name: Download target directories (3.5.0) 332 | uses: actions/download-artifact@v4 333 | with: 334 | name: target-${{ matrix.os }}-${{ matrix.java }}-3.5.0 335 | 336 | - name: Inflate target directories (3.5.0) 337 | run: | 338 | tar xf targets.tar 339 | rm targets.tar 340 | 341 | - name: Download target directories (3.5.1) 342 | uses: actions/download-artifact@v4 343 | with: 344 | name: target-${{ matrix.os }}-${{ matrix.java }}-3.5.1 345 | 346 | - name: Inflate target directories (3.5.1) 347 | run: | 348 | tar xf targets.tar 349 | rm targets.tar 350 | 351 | - name: Download target directories (3.5.2) 352 | uses: actions/download-artifact@v4 353 | with: 354 | name: target-${{ matrix.os }}-${{ matrix.java }}-3.5.2 355 | 356 | - name: Inflate target directories (3.5.2) 357 | run: | 358 | tar xf targets.tar 359 | rm targets.tar 360 | 361 | - name: Download target directories (3.6.2) 362 | uses: actions/download-artifact@v4 363 | with: 364 | name: target-${{ matrix.os }}-${{ matrix.java }}-3.6.2 365 | 366 | - name: Inflate target directories (3.6.2) 367 | run: | 368 | tar xf targets.tar 369 | rm targets.tar 370 | 371 | - name: Download target directories (3.6.3) 372 | uses: actions/download-artifact@v4 373 | with: 374 | name: target-${{ matrix.os }}-${{ matrix.java }}-3.6.3 375 | 376 | - name: Inflate target directories (3.6.3) 377 | run: | 378 | tar xf targets.tar 379 | rm targets.tar 380 | 381 | - name: Download target directories (3.6.4) 382 | uses: actions/download-artifact@v4 383 | with: 384 | name: target-${{ matrix.os }}-${{ matrix.java }}-3.6.4 385 | 386 | - name: Inflate target directories (3.6.4) 387 | run: | 388 | tar xf targets.tar 389 | rm targets.tar 390 | 391 | - name: Download target directories (3.7.0) 392 | uses: actions/download-artifact@v4 393 | with: 394 | name: target-${{ matrix.os }}-${{ matrix.java }}-3.7.0 395 | 396 | - name: Inflate target directories (3.7.0) 397 | run: | 398 | tar xf targets.tar 399 | rm targets.tar 400 | 401 | - name: Download target directories (3.7.1) 402 | uses: actions/download-artifact@v4 403 | with: 404 | name: target-${{ matrix.os }}-${{ matrix.java }}-3.7.1 405 | 406 | - name: Inflate target directories (3.7.1) 407 | run: | 408 | tar xf targets.tar 409 | rm targets.tar 410 | 411 | - name: Import signing key 412 | if: env.PGP_SECRET != '' && env.PGP_PASSPHRASE == '' 413 | env: 414 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 415 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 416 | run: echo $PGP_SECRET | base64 -d -i - | gpg --import 417 | 418 | - name: Import signing key and strip passphrase 419 | if: env.PGP_SECRET != '' && env.PGP_PASSPHRASE != '' 420 | env: 421 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 422 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 423 | run: | 424 | echo "$PGP_SECRET" | base64 -d -i - > /tmp/signing-key.gpg 425 | echo "$PGP_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --import /tmp/signing-key.gpg 426 | (echo "$PGP_PASSPHRASE"; echo; echo) | gpg --command-fd 0 --pinentry-mode loopback --change-passphrase $(gpg --list-secret-keys --with-colons 2> /dev/null | grep '^sec:' | cut --delimiter ':' --fields 5 | tail -n 1) 427 | 428 | - name: Publish 429 | env: 430 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 431 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 432 | SONATYPE_CREDENTIAL_HOST: ${{ secrets.SONATYPE_CREDENTIAL_HOST }} 433 | run: sbt tlCiRelease 434 | 435 | dependency-submission: 436 | name: Submit Dependencies 437 | if: github.event.repository.fork == false && github.event_name != 'pull_request' 438 | strategy: 439 | matrix: 440 | os: [ubuntu-22.04] 441 | java: [temurin@8] 442 | runs-on: ${{ matrix.os }} 443 | steps: 444 | - name: Checkout current branch (full) 445 | uses: actions/checkout@v4 446 | with: 447 | fetch-depth: 0 448 | 449 | - name: Setup sbt 450 | uses: sbt/setup-sbt@v1 451 | 452 | - name: Setup Java (temurin@8) 453 | id: setup-java-temurin-8 454 | if: matrix.java == 'temurin@8' 455 | uses: actions/setup-java@v4 456 | with: 457 | distribution: temurin 458 | java-version: 8 459 | cache: sbt 460 | 461 | - name: sbt update 462 | if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' 463 | run: sbt +update 464 | 465 | - name: Submit Dependencies 466 | uses: scalacenter/sbt-dependency-submission@v2 467 | with: 468 | modules-ignore: root_2.12 root_2.12 root_2.12 root_2.13 root_2.13 root_2.13 root_2.13 root_2.13 root_3 root_3 root_3 root_3 root_3 root_3 root_3 root_3 root_3 root_3 root_3 root_3 root_3 root_3 root_3 root_3 root_3 root_3 tests_2.12 tests_2.12 tests_2.12 tests_2.13 tests_2.13 tests_2.13 tests_2.13 tests_2.13 tests_3 tests_3 tests_3 tests_3 tests_3 tests_3 tests_3 tests_3 tests_3 tests_3 tests_3 tests_3 tests_3 tests_3 tests_3 tests_3 tests_3 tests_3 469 | configs-ignore: test scala-tool scala-doc-tool test-internal 470 | -------------------------------------------------------------------------------- /.github/workflows/clean.yml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by sbt-github-actions using the 2 | # githubWorkflowGenerate task. You should add and commit this file to 3 | # your git repository. It goes without saying that you shouldn't edit 4 | # this file by hand! Instead, if you wish to make changes, you should 5 | # change your sbt build configuration to revise the workflow description 6 | # to meet your needs, then regenerate this file. 7 | 8 | name: Clean 9 | 10 | on: push 11 | 12 | jobs: 13 | delete-artifacts: 14 | name: Delete Artifacts 15 | runs-on: ubuntu-latest 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | steps: 19 | - name: Delete artifacts 20 | run: | 21 | # Customize those three lines with your repository and credentials: 22 | REPO=${GITHUB_API_URL}/repos/${{ github.repository }} 23 | 24 | # A shortcut to call GitHub API. 25 | ghapi() { curl --silent --location --user _:$GITHUB_TOKEN "$@"; } 26 | 27 | # A temporary file which receives HTTP response headers. 28 | TMPFILE=/tmp/tmp.$$ 29 | 30 | # An associative array, key: artifact name, value: number of artifacts of that name. 31 | declare -A ARTCOUNT 32 | 33 | # Process all artifacts on this repository, loop on returned "pages". 34 | URL=$REPO/actions/artifacts 35 | while [[ -n "$URL" ]]; do 36 | 37 | # Get current page, get response headers in a temporary file. 38 | JSON=$(ghapi --dump-header $TMPFILE "$URL") 39 | 40 | # Get URL of next page. Will be empty if we are at the last page. 41 | URL=$(grep '^Link:' "$TMPFILE" | tr ',' '\n' | grep 'rel="next"' | head -1 | sed -e 's/.*.*//') 42 | rm -f $TMPFILE 43 | 44 | # Number of artifacts on this page: 45 | COUNT=$(( $(jq <<<$JSON -r '.artifacts | length') )) 46 | 47 | # Loop on all artifacts on this page. 48 | for ((i=0; $i < $COUNT; i++)); do 49 | 50 | # Get name of artifact and count instances of this name. 51 | name=$(jq <<<$JSON -r ".artifacts[$i].name?") 52 | ARTCOUNT[$name]=$(( $(( ${ARTCOUNT[$name]} )) + 1)) 53 | 54 | id=$(jq <<<$JSON -r ".artifacts[$i].id?") 55 | size=$(( $(jq <<<$JSON -r ".artifacts[$i].size_in_bytes?") )) 56 | printf "Deleting '%s' #%d, %'d bytes\n" $name ${ARTCOUNT[$name]} $size 57 | ghapi -X DELETE $REPO/actions/artifacts/$id 58 | done 59 | done 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .metals/ 2 | *.class 3 | .idea 4 | .bloop 5 | target/ 6 | **/secret.conf 7 | **/.DS_Store 8 | .vscode/ 9 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by sbt-typelevel-mergify using the 2 | # mergifyGenerate task. You should add and commit this file to 3 | # your git repository. It goes without saying that you shouldn't edit 4 | # this file by hand! Instead, if you wish to make changes, you should 5 | # change your sbt build configuration to revise the mergify configuration 6 | # to meet your needs, then regenerate this file. 7 | 8 | pull_request_rules: 9 | - name: merge scala-steward's PRs 10 | conditions: 11 | - author=scala-steward 12 | - body~=labels:.*early-semver-patch 13 | - status-success=Test (ubuntu-22.04, 2.12.18, temurin@8) 14 | - status-success=Test (ubuntu-22.04, 2.12.19, temurin@8) 15 | - status-success=Test (ubuntu-22.04, 2.12.20, temurin@8) 16 | - status-success=Test (ubuntu-22.04, 2.13.12, temurin@8) 17 | - status-success=Test (ubuntu-22.04, 2.13.13, temurin@8) 18 | - status-success=Test (ubuntu-22.04, 2.13.14, temurin@8) 19 | - status-success=Test (ubuntu-22.04, 2.13.15, temurin@8) 20 | - status-success=Test (ubuntu-22.04, 2.13.16, temurin@8) 21 | - status-success=Test (ubuntu-22.04, 3.3.0, temurin@8) 22 | - status-success=Test (ubuntu-22.04, 3.3.1, temurin@8) 23 | - status-success=Test (ubuntu-22.04, 3.3.3, temurin@8) 24 | - status-success=Test (ubuntu-22.04, 3.3.4, temurin@8) 25 | - status-success=Test (ubuntu-22.04, 3.3.5, temurin@8) 26 | - status-success=Test (ubuntu-22.04, 3.3.6, temurin@8) 27 | - status-success=Test (ubuntu-22.04, 3.4.0, temurin@8) 28 | - status-success=Test (ubuntu-22.04, 3.4.1, temurin@8) 29 | - status-success=Test (ubuntu-22.04, 3.4.2, temurin@8) 30 | - status-success=Test (ubuntu-22.04, 3.4.3, temurin@8) 31 | - status-success=Test (ubuntu-22.04, 3.5.0, temurin@8) 32 | - status-success=Test (ubuntu-22.04, 3.5.1, temurin@8) 33 | - status-success=Test (ubuntu-22.04, 3.5.2, temurin@8) 34 | - status-success=Test (ubuntu-22.04, 3.6.2, temurin@8) 35 | - status-success=Test (ubuntu-22.04, 3.6.3, temurin@8) 36 | - status-success=Test (ubuntu-22.04, 3.6.4, temurin@8) 37 | - status-success=Test (ubuntu-22.04, 3.7.0, temurin@8) 38 | - status-success=Test (ubuntu-22.04, 3.7.1, temurin@8) 39 | actions: 40 | merge: {} 41 | - name: Label plugin PRs 42 | conditions: 43 | - files~=^plugin/ 44 | actions: 45 | label: 46 | add: 47 | - plugin 48 | remove: [] 49 | - name: Label tests PRs 50 | conditions: 51 | - files~=^tests/ 52 | actions: 53 | label: 54 | add: 55 | - tests 56 | remove: [] 57 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "3.9.7" 2 | runner.dialect = scala3 3 | rewrite.scala3.insertEndMarkerMinLines = 50 4 | maxColumn = 140 5 | align.preset = some 6 | align.tokens.add = [ 7 | {code = "<-", owner = Enumerator.Generator} 8 | ] 9 | align.multiline = true 10 | align.arrowEnumeratorGenerator = true 11 | 12 | newlines.topLevelStatements = [before, after] 13 | newlines.implicitParamListModifierForce = [before] 14 | newlines.topLevelStatementsMinBreaks = 2 15 | continuationIndent.defnSite = 2 16 | continuationIndent.extendSite = 2 17 | 18 | optIn.breakChainOnFirstMethodDot = true 19 | includeCurlyBraceInSelectChains = true 20 | includeNoParensInSelectChains = true 21 | 22 | rewrite.rules = [ 23 | RedundantBraces, 24 | RedundantParens, 25 | ExpandImportSelectors, 26 | PreferCurlyFors 27 | ] 28 | 29 | runner.optimizer.forceConfigStyleMinArgCount = 3 30 | danglingParentheses.defnSite = true 31 | danglingParentheses.callSite = true 32 | danglingParentheses.exclude = [ 33 | "`trait`" 34 | ] 35 | verticalMultiline.newlineAfterOpenParen = true 36 | verticalMultiline.atDefnSite = true 37 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing guide 2 | 3 | ## Adding new features 4 | 5 | If you want to add a new feature, check if it's already been discussed in the issues list. 6 | 7 | Before you start working on an existing feature / bugfix, let us know you're taking it on in its comments :) 8 | 9 | ## Adding a new Scala version 10 | 11 | 1. Add it to `./scala-versions` (one version per line) 12 | 2. Run `sbt generateAll` 13 | 3. Commit and open a pull request. 14 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright 2020 Jakub Kozłowski 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # better-tostring 2 | 3 | [![License](http://img.shields.io/:license-Apache%202-green.svg)](http://www.apache.org/licenses/LICENSE-2.0.txt) 4 | [![Latest version](https://index.scala-lang.org/polyvariant/better-tostring/better-tostring/latest.svg)](https://index.scala-lang.org/kubukoz/better-tostring/better-tostring) 5 | [![Maven Central](https://img.shields.io/maven-central/v/org.polyvariant/better-tostring_2.13.5.svg)](http://search.maven.org/#search%7Cga%7C1%7Cbetter-tostring) 6 | 7 | A Scala compiler plugin that replaces the default `toString` implementation of case classes with a more verbose one. 8 | 9 | ## Example 10 | 11 | Without the plugin: 12 | 13 | ```scala 14 | final case class User(name: String, age: Int) 15 | 16 | User("Joe", 18).toString() // "User(Joe, 18)" 17 | ``` 18 | 19 | With the plugin: 20 | 21 | ```scala 22 | User("Joe", 18).toString() // "User(name = Joe, age = 18)" 23 | ``` 24 | 25 | ## Usage 26 | 27 | In sbt: 28 | 29 | ```scala 30 | libraryDependencies += compilerPlugin("org.polyvariant" % "better-tostring" % "0.3.17" cross CrossVersion.full) 31 | ``` 32 | 33 | In scala-cli: 34 | 35 | ```scala 36 | //> using plugin "org.polyvariant:::better-tostring:0.3.17" 37 | ``` 38 | 39 | (note: versions before `0.3.8` were published under the `com.kubukoz` organization instead of `org.polyvariant`) 40 | 41 | 42 | The plugin is currently published for the following 26 Scala versions: 43 | 44 | - 2.12.18, 2.12.19, 2.12.20 45 | - 2.13.12, 2.13.13, 2.13.14, 2.13.15, 2.13.16 46 | - 3.3.0, 3.3.1, 3.3.3, 3.3.4, 3.3.5, 3.3.6 47 | - 3.4.0, 3.4.1, 3.4.2, 3.4.3 48 | - 3.5.0, 3.5.1, 3.5.2 49 | - 3.6.2, 3.6.3, 3.6.4 50 | - 3.7.0, 3.7.1 51 | 52 | 53 | For older Scala versions, see [previous versions of better-tostring](https://repo1.maven.org/maven2/org/polyvariant) ([or even older versions](https://repo1.maven.org/maven2/com/kubukoz)). 54 | 55 | As a rule of thumb, active support will include _at least_ 3 latest stable versions of 2.12, 2.13 and 3.0 for the foreseeable future. 56 | 57 | ## What does the plugin actually do? 58 | 59 | 1. Only case classes located directly in `package`s or `object`s are changed. Nested classes and classes local to functions are currently ignored. 60 | 2. Only the fields in the first parameter list are shown. 61 | 3. If the class is nested in an object (but not a package object), the object's name and a dot are prepended. 62 | 4. If the class already overrides `toString` *directly*, it's not replaced. 63 | 64 | ## Roadmap 65 | 66 | - Ignore classes that inherit `toString` from a type that isn't `Object` (#34) 67 | - Potentially ignore value classes (#19) 68 | 69 | If you have ideas for improving the plugin, feel free to create an issue and we'll consider making it happen :) 70 | 71 | ## Customization? 72 | 73 | _tl;dr there is none._ 74 | 75 | The plugin makes certain assumptions about what is a _better_ `toString`. We aim for a useful and reasonably verbose description of the data type, 76 | which could make it easier to find certain issues with your tests (mismatching values in a field) or see the labels in debug logs. 77 | 78 | We also want the plugin to become minimal in the implementation and easy to use (plug & play), without lots of configuration options, so the representation of the data types will not be customizable. **The format may change over time without prior notice**, so you shouldn't rely on the exact representation (as is the case with any `toString` methods), but any changes in behavior will be communicated in the release notes. 79 | 80 | If you need a different `toString`, we suggest that you implement one yourself. You may also want to look at [pprint](https://github.com/com-lihaoyi/PPrint). 81 | 82 | ## Contributing 83 | 84 | See [CONTRIBUTING.md](./CONTRIBUTING.md) 85 | 86 | ## Maintainers 87 | 88 | The maintainers of this project (people who can merge PRs and make releases) are: 89 | 90 | - Jakub Kozłowski ([@kubukoz](https://github.com/kubukoz)) 91 | - Michał Pawlik ([@majk-p](https://github.com/majk-p)) 92 | - Mikołaj Robakowski ([@mrobakowski](https://github.com/mrobakowski)) 93 | 94 | ## Community 95 | 96 | This project supports the [Scala code of conduct](https://www.scala-lang.org/conduct/) and wants communication on all its channels (GitHub etc.) to be inclusive environments. 97 | 98 | If you have any concerns about someone's behavior on these channels, contact [Jakub Kozłowski](mailto:kubukoz@gmail.com). 99 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | ThisBuild / tlBaseVersion := "0.3" 2 | ThisBuild / organization := "org.polyvariant" 3 | ThisBuild / organizationName := "Polyvariant" 4 | ThisBuild / startYear := Some(2020) 5 | ThisBuild / licenses := Seq(License.Apache2) 6 | ThisBuild / developers := List( 7 | tlGitHubDev("kubukoz", "Jakub Kozłowski"), 8 | tlGitHubDev("majk-p", "Michał Pawlik") 9 | ) 10 | Global / onChangedBuildSource := ReloadOnSourceChanges 11 | 12 | ThisBuild / tlFatalWarnings := false 13 | 14 | Global / onChangedBuildSource := ReloadOnSourceChanges 15 | 16 | // for dottydoc 17 | ThisBuild / resolvers += Resolver.JCenterRepository 18 | 19 | ThisBuild / scalaVersion := "3.3.0" 20 | ThisBuild / crossScalaVersions := IO.read(file("scala-versions")).split("\n").map(_.trim) 21 | 22 | ThisBuild / githubWorkflowEnv ++= List( 23 | "PGP_PASSPHRASE", 24 | "PGP_SECRET", 25 | "SONATYPE_PASSWORD", 26 | "SONATYPE_USERNAME" 27 | ).map { envKey => 28 | envKey -> s"$${{ secrets.$envKey }}" 29 | }.toMap 30 | 31 | ThisBuild / githubWorkflowPublishTargetBranches := List(RefPredicate.StartsWith(Ref.Tag("v"))) 32 | 33 | ThisBuild / githubWorkflowGeneratedCI ~= { 34 | _.map { 35 | case job if job.id == "build" => 36 | job.withSteps( 37 | job.steps.map { 38 | case step: WorkflowStep.Sbt if step.name == Some("Check that workflows are up to date") => 39 | step.withCommands(List("githubWorkflowCheck", "readmeCheck")) 40 | case step => step 41 | } 42 | ) 43 | case job => job 44 | } 45 | } 46 | 47 | val commonSettings = Seq( 48 | scalacOptions --= Seq("-source:3.0-migration"), 49 | mimaPreviousArtifacts := Set.empty, 50 | // We don't need KP 51 | libraryDependencies -= compilerPlugin("org.typelevel" % "kind-projector" % "0.13.2" cross CrossVersion.full) 52 | ) 53 | 54 | val plugin = project 55 | .settings( 56 | name := "better-tostring", 57 | commonSettings, 58 | crossTarget := target.value / s"scala-${scalaVersion.value}", // workaround for https://github.com/sbt/sbt/issues/5097 59 | crossVersion := CrossVersion.full, 60 | libraryDependencies ++= Seq( 61 | scalaOrganization.value % ( 62 | if (scalaVersion.value.startsWith("3")) 63 | s"scala3-compiler_3" 64 | else "scala-compiler" 65 | ) % scalaVersion.value 66 | ), 67 | // 3.3.x -> "scala-3.3.x" 68 | Compile / unmanagedSourceDirectories += 69 | sourceDirectory.value / "main" / s"scala-${scalaVersion.value.split("\\.").take(2).mkString(".")}.x" 70 | ) 71 | .enablePlugins(BackpublishPlugin) 72 | 73 | val tests = project 74 | .settings( 75 | commonSettings, 76 | scalacOptions ++= { 77 | val jar = (plugin / Compile / packageBin).value 78 | Seq( 79 | s"-Xplugin:${jar.getAbsolutePath}", 80 | s"-Xplugin-require:better-tostring", 81 | s"-Jdummy=${jar.lastModified}" 82 | ) // borrowed from bm4 83 | }, 84 | libraryDependencies ++= Seq("org.scalameta" %% "munit" % "1.1.1" % Test), 85 | buildInfoKeys ++= Seq(scalaVersion), 86 | buildInfoPackage := "b2s.buildinfo", 87 | Compile / doc / sources := Seq() 88 | ) 89 | .enablePlugins(BuildInfoPlugin, NoPublishPlugin) 90 | 91 | val betterToString = 92 | project 93 | .in(file(".")) 94 | .settings(name := "root") 95 | .settings( 96 | commonSettings, 97 | (publish / skip) := true, 98 | addCommandAlias("generateAll", List("githubWorkflowGenerate", "mergifyGenerate", "readmeWrite").mkString(";")) 99 | ) 100 | .aggregate(plugin, tests) 101 | .enablePlugins(NoPublishPlugin) 102 | .enablePlugins(ReadmePlugin) 103 | -------------------------------------------------------------------------------- /plugin/src/main/resources/plugin.properties: -------------------------------------------------------------------------------- 1 | pluginClass=org.polyvariant.BetterToStringPlugin 2 | -------------------------------------------------------------------------------- /plugin/src/main/resources/scalac-plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | better-tostring 3 | org.polyvariant.BetterToStringPlugin 4 | 5 | -------------------------------------------------------------------------------- /plugin/src/main/scala-2/BetterToStringPlugin.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Polyvariant 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.polyvariant 18 | 19 | import scala.tools.nsc.Global 20 | import scala.tools.nsc.Phase 21 | import scala.tools.nsc.plugins.Plugin 22 | import scala.tools.nsc.plugins.PluginComponent 23 | import scala.tools.nsc.transform.TypingTransformers 24 | 25 | final class BetterToStringPlugin(override val global: Global) extends Plugin { 26 | override val name: String = "better-tostring" 27 | override val description: String = 28 | "scala compiler plugin for better default toString implementations" 29 | 30 | override val components: List[PluginComponent] = List( 31 | new BetterToStringPluginComponent(global) 32 | ) 33 | 34 | } 35 | 36 | final class BetterToStringPluginComponent(val global: Global) extends PluginComponent with TypingTransformers { 37 | import global._ 38 | override val phaseName: String = "better-tostring-phase" 39 | override val runsAfter: List[String] = List("parser") 40 | 41 | private val api: Scala2CompilerApi[global.type] = Scala2CompilerApi.instance(global) 42 | private val impl = BetterToStringImpl.instance(api) 43 | 44 | private def modifyClasses(tree: Tree, enclosingObject: Option[ModuleDef]): Tree = 45 | tree match { 46 | case p: PackageDef => p.copy(stats = p.stats.map(modifyClasses(_, None))) 47 | 48 | case m: ModuleDef if m.mods.isCase => 49 | // isNested=false for the same reason as in the ClassDef case 50 | impl.transformClass(api.Classable.Obj(m), isNested = false, enclosingObject).merge 51 | 52 | case m: ModuleDef => 53 | m.copy(impl = m.impl.copy(body = m.impl.body.map(modifyClasses(_, Some(m))))) 54 | 55 | case clazz: ClassDef => 56 | impl 57 | .transformClass( 58 | api.Classable.Clazz(clazz), 59 | // If it was nested, we wouldn't be in this branch. 60 | // Scala 2.x compiler API limitation (classes can't tell what the owner is). 61 | // This should be more optimal as we don't traverse every template, but it hasn't been benchmarked. 62 | isNested = false, 63 | enclosingObject 64 | ) 65 | .merge 66 | 67 | case other => other 68 | } 69 | 70 | override def newPhase(prev: Phase): Phase = new StdPhase(prev) { 71 | 72 | override def apply(unit: CompilationUnit): Unit = 73 | new Transformer { 74 | override def transform(tree: Tree): Tree = modifyClasses(tree, None) 75 | }.transformUnit(unit) 76 | 77 | } 78 | 79 | } 80 | -------------------------------------------------------------------------------- /plugin/src/main/scala-2/Scala2CompilerApi.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Polyvariant 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.polyvariant 18 | 19 | import scala.reflect.internal.Flags 20 | import scala.tools.nsc.Global 21 | 22 | trait Scala2CompilerApi[G <: Global] extends CompilerApi { 23 | val theGlobal: G 24 | import theGlobal._ 25 | 26 | sealed trait Classable extends Product with Serializable { 27 | 28 | def bimap( 29 | clazz: ClassDef => ClassDef, 30 | obj: ModuleDef => ModuleDef 31 | ): Classable = this match { 32 | case Classable.Clazz(c) => Classable.Clazz(clazz(c)) 33 | case Classable.Obj(o) => Classable.Obj(obj(o)) 34 | } 35 | 36 | def fold[A]( 37 | clazz: ClassDef => A, 38 | obj: ModuleDef => A 39 | ): A = this match { 40 | case Classable.Clazz(c) => clazz(c) 41 | case Classable.Obj(o) => obj(o) 42 | } 43 | 44 | def merge: ImplDef = fold(identity, identity) 45 | 46 | } 47 | 48 | object Classable { 49 | case class Clazz(c: ClassDef) extends Classable 50 | case class Obj(o: ModuleDef) extends Classable 51 | } 52 | 53 | type Tree = theGlobal.Tree 54 | type Clazz = Classable 55 | type Param = ValDef 56 | type ParamName = TermName 57 | type Method = DefDef 58 | type EnclosingObject = ModuleDef 59 | } 60 | 61 | object Scala2CompilerApi { 62 | 63 | def instance(global: Global): Scala2CompilerApi[global.type] = 64 | new Scala2CompilerApi[global.type] { 65 | val theGlobal: global.type = global 66 | import global._ 67 | 68 | def params(clazz: Clazz): List[Param] = clazz.fold( 69 | clazz = _.impl.body.collect { 70 | case v: ValDef if v.mods.isCaseAccessor => v 71 | }, 72 | obj = _ => Nil 73 | ) 74 | 75 | def className(clazz: Clazz): String = clazz.merge.name.toString 76 | 77 | def isPackageOrPackageObject(enclosingObject: EnclosingObject): Boolean = 78 | // couldn't find any nice api for this. `m.symbol.isPackageObject` does not work after the parser compiler phase (needs to run later). 79 | enclosingObject.symbol.isInstanceOf[NoSymbol] && enclosingObject.name.toString == "package" 80 | 81 | def enclosingObjectName(enclosingObject: EnclosingObject): String = enclosingObject.name.toString 82 | def literalConstant(value: String): Tree = Literal(Constant(value)) 83 | def paramName(param: Param): ParamName = param.name 84 | def selectInThis(clazz: Clazz, name: ParamName): Tree = q"this.$name" 85 | def concat(l: Tree, r: Tree): Tree = q"$l + $r" 86 | 87 | def createToString(clazz: Clazz, body: Tree): Method = DefDef( 88 | Modifiers(Flags.OVERRIDE), 89 | TermName("toString"), 90 | Nil, 91 | List(List()), 92 | Ident(TypeName("String")), 93 | body 94 | ) 95 | 96 | def addMethod(clazz: Clazz, method: Method): Clazz = { 97 | val newBody = clazz.merge.impl.copy(body = clazz.merge.impl.body :+ method) 98 | clazz.bimap( 99 | clazz = _.copy(impl = newBody), 100 | obj = _.copy(impl = newBody) 101 | ) 102 | } 103 | 104 | def methodNames(clazz: Clazz): List[String] = clazz.merge.impl.body.collect { 105 | case d: DefDef => d.name.toString 106 | case d: ValDef => d.name.toString 107 | } 108 | 109 | def isCaseClass(clazz: Clazz): Boolean = clazz.merge.mods.isCase 110 | 111 | // Always return true for ModuleDef - apparently ModuleDef doesn't have the module flag... 112 | def isObject(clazz: Clazz): Boolean = clazz.fold( 113 | clazz = _.mods.hasModuleFlag, 114 | obj = _ => true 115 | ) 116 | 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /plugin/src/main/scala-3.1.x/AfterPhase.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Polyvariant 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.polyvariant 18 | 19 | val AfterPhase = dotty.tools.dotc.typer.TyperPhase 20 | -------------------------------------------------------------------------------- /plugin/src/main/scala-3.2.x/AfterPhase.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Polyvariant 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.polyvariant 18 | 19 | val AfterPhase = dotty.tools.dotc.typer.TyperPhase 20 | -------------------------------------------------------------------------------- /plugin/src/main/scala-3.3.x/AfterPhase.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Polyvariant 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.polyvariant 18 | 19 | val AfterPhase = dotty.tools.dotc.typer.TyperPhase 20 | -------------------------------------------------------------------------------- /plugin/src/main/scala-3.4.x/AfterPhase.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Polyvariant 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.polyvariant 18 | 19 | val AfterPhase = dotty.tools.dotc.typer.TyperPhase 20 | -------------------------------------------------------------------------------- /plugin/src/main/scala-3.5.x/AfterPhase.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Polyvariant 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.polyvariant 18 | 19 | val AfterPhase = dotty.tools.dotc.typer.TyperPhase 20 | -------------------------------------------------------------------------------- /plugin/src/main/scala-3.6.x/AfterPhase.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Polyvariant 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.polyvariant 18 | 19 | val AfterPhase = dotty.tools.dotc.typer.TyperPhase 20 | -------------------------------------------------------------------------------- /plugin/src/main/scala-3.7.x/AfterPhase.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Polyvariant 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.polyvariant 18 | 19 | val AfterPhase = dotty.tools.dotc.typer.TyperPhase 20 | -------------------------------------------------------------------------------- /plugin/src/main/scala-3/BetterToStringPlugin.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Polyvariant 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.polyvariant 18 | 19 | import dotty.tools.dotc.ast.tpd 20 | import dotty.tools.dotc.core.Contexts.Context 21 | import dotty.tools.dotc.core.Flags.Module 22 | import dotty.tools.dotc.core.Flags.Package 23 | import dotty.tools.dotc.core.Symbols 24 | import dotty.tools.dotc.plugins.PluginPhase 25 | import dotty.tools.dotc.plugins.StandardPlugin 26 | 27 | import scala.annotation.tailrec 28 | 29 | import tpd.* 30 | 31 | final class BetterToStringPlugin extends StandardPlugin: 32 | override val name: String = "better-tostring" 33 | override val description: String = "scala compiler plugin for better default toString implementations" 34 | override def init(options: List[String]): List[PluginPhase] = List(new BetterToStringPluginPhase) 35 | 36 | final class BetterToStringPluginPhase extends PluginPhase: 37 | 38 | override val phaseName: String = "better-tostring-phase" 39 | override val runsAfter: Set[String] = Set(org.polyvariant.AfterPhase.name) 40 | 41 | override def transformTemplate( 42 | t: Template 43 | )( 44 | using ctx: Context 45 | ): Tree = 46 | val clazz = ctx.owner.asClass 47 | 48 | val ownerOwner = ctx.owner.owner 49 | val isNested = ownerOwner.ownersIterator.exists(!_.is(Module)) 50 | 51 | val enclosingObject = 52 | if ownerOwner.is(Module) then Some(ownerOwner) 53 | else None 54 | 55 | BetterToStringImpl 56 | .instance(Scala3CompilerApi.instance) 57 | .transformClass(Scala3CompilerApi.ClassContext(t, clazz), isNested, enclosingObject) 58 | .t 59 | -------------------------------------------------------------------------------- /plugin/src/main/scala-3/Scala3CompilerApi.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Polyvariant 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.polyvariant 18 | 19 | import dotty.tools.dotc.ast.Trees 20 | import dotty.tools.dotc.ast.tpd 21 | import dotty.tools.dotc.core.Constants.Constant 22 | import dotty.tools.dotc.core.Contexts.Context 23 | import dotty.tools.dotc.core.Decorators.* 24 | import dotty.tools.dotc.core.Flags 25 | import dotty.tools.dotc.core.Flags.CaseAccessor 26 | import dotty.tools.dotc.core.Flags.CaseClass 27 | import dotty.tools.dotc.core.Flags.Module 28 | import dotty.tools.dotc.core.Flags.Override 29 | import dotty.tools.dotc.core.Flags.Package 30 | import dotty.tools.dotc.core.Names 31 | import dotty.tools.dotc.core.Symbols 32 | import dotty.tools.dotc.core.Symbols.ClassSymbol 33 | import dotty.tools.dotc.core.Types 34 | 35 | import tpd.* 36 | 37 | trait Scala3CompilerApi extends CompilerApi: 38 | type Tree = Trees.Tree[Types.Type] 39 | type Clazz = Scala3CompilerApi.ClassContext 40 | type Param = ValDef 41 | type ParamName = Names.TermName 42 | type Method = DefDef 43 | type EnclosingObject = Symbols.Symbol 44 | 45 | object Scala3CompilerApi: 46 | final case class ClassContext(t: Template, clazz: ClassSymbol): 47 | def mapTemplate(f: Template => Template): ClassContext = copy(t = f(t)) 48 | 49 | def instance( 50 | using Context 51 | ): Scala3CompilerApi = new Scala3CompilerApi: 52 | 53 | def params(clazz: Clazz): List[Param] = 54 | clazz.t.body.collect { 55 | case v: ValDef if v.mods.is(CaseAccessor) => v 56 | } 57 | 58 | def className(clazz: Clazz): String = 59 | clazz.clazz.originalName.toString 60 | 61 | def isPackageOrPackageObject(enclosingObject: EnclosingObject): Boolean = 62 | enclosingObject.is(Package) || enclosingObject.isPackageObject 63 | 64 | def enclosingObjectName(enclosingObject: EnclosingObject): String = 65 | enclosingObject.effectiveName.toString 66 | 67 | def literalConstant(value: String): Tree = Literal(Constant(value)) 68 | def paramName(param: Param): ParamName = param.name 69 | def selectInThis(clazz: Clazz, name: ParamName): Tree = This(clazz.clazz).select(name) 70 | def concat(l: Tree, r: Tree): Tree = l.select("+".toTermName).appliedTo(r) 71 | 72 | def createToString(owner: Clazz, body: Tree): Method = { 73 | val clazz = owner.clazz 74 | // this was adapted from dotty.tools.dotc.transform.SyntheticMembers (line 115) 75 | val sym = Symbols.defn.Any_toString 76 | 77 | val toStringSymbol = sym 78 | .copy( 79 | owner = clazz, 80 | flags = sym.flags | Override, 81 | info = clazz.thisType.memberInfo(sym), 82 | coord = clazz.coord 83 | ) 84 | .entered 85 | .asTerm 86 | 87 | DefDef(toStringSymbol, body) 88 | } 89 | 90 | def addMethod(clazz: Clazz, method: Method): Clazz = 91 | clazz.mapTemplate(t => cpy.Template(t)(body = t.body :+ method)) 92 | 93 | // note: also returns vals because why not 94 | def methodNames(clazz: Clazz): List[String] = 95 | clazz.t.body.collect { case d: (DefDef | ValDef) => 96 | d.name.toString 97 | } 98 | 99 | def isCaseClass(clazz: Clazz): Boolean = 100 | // for some reason, this is true for case objects too 101 | clazz.clazz.flags.is(CaseClass) 102 | 103 | def isObject(clazz: Clazz): Boolean = 104 | clazz.clazz.flags.is(Module) 105 | 106 | end Scala3CompilerApi 107 | -------------------------------------------------------------------------------- /plugin/src/main/scala/BetterToStringImpl.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Polyvariant 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.polyvariant 18 | 19 | // Source-compatible core between 2.x and 3.x implementations 20 | 21 | trait CompilerApi { 22 | type Tree 23 | type Clazz 24 | type Param 25 | type ParamName 26 | type Method 27 | type EnclosingObject 28 | 29 | def className(clazz: Clazz): String 30 | def isPackageOrPackageObject(enclosingObject: EnclosingObject): Boolean 31 | def enclosingObjectName(enclosingObject: EnclosingObject): String 32 | def params(clazz: Clazz): List[Param] 33 | def literalConstant(value: String): Tree 34 | 35 | def paramName(param: Param): ParamName 36 | def selectInThis(clazz: Clazz, name: ParamName): Tree 37 | def concat(l: Tree, r: Tree): Tree 38 | 39 | def createToString(clazz: Clazz, body: Tree): Method 40 | def addMethod(clazz: Clazz, method: Method): Clazz 41 | def methodNames(clazz: Clazz): List[String] 42 | // better name: "is case class or object" 43 | def isCaseClass(clazz: Clazz): Boolean 44 | def isObject(clazz: Clazz): Boolean 45 | } 46 | 47 | trait BetterToStringImpl[+C <: CompilerApi] { 48 | val compilerApi: C 49 | 50 | def transformClass( 51 | clazz: compilerApi.Clazz, 52 | isNested: Boolean, 53 | enclosingObject: Option[compilerApi.EnclosingObject] 54 | ): compilerApi.Clazz 55 | 56 | } 57 | 58 | object BetterToStringImpl { 59 | 60 | def instance( 61 | api: CompilerApi 62 | ): BetterToStringImpl[api.type] = 63 | new BetterToStringImpl[api.type] { 64 | val compilerApi: api.type = api 65 | 66 | import api._ 67 | 68 | def transformClass( 69 | clazz: Clazz, 70 | isNested: Boolean, 71 | enclosingObject: Option[EnclosingObject] 72 | ): Clazz = { 73 | // technically, the method found by this can be even something like "def toString(s: String): Unit", but we're ignoring that 74 | val hasToString: Boolean = methodNames(clazz).contains("toString") 75 | 76 | val shouldModify = isCaseClass(clazz) && !isNested && !hasToString 77 | 78 | if (shouldModify) overrideToString(clazz, enclosingObject) 79 | else clazz 80 | } 81 | 82 | private def overrideToString(clazz: Clazz, enclosingObject: Option[EnclosingObject]): Clazz = 83 | addMethod(clazz, createToString(clazz, toStringImpl(clazz, enclosingObject))) 84 | 85 | private def toStringImpl(clazz: Clazz, enclosingObject: Option[EnclosingObject]): Tree = { 86 | val className = api.className(clazz) 87 | val parentPrefix = enclosingObject.filterNot(api.isPackageOrPackageObject).fold("")(api.enclosingObjectName(_) ++ ".") 88 | 89 | val namePart = literalConstant(parentPrefix ++ className) 90 | 91 | val paramListParts: List[Tree] = params(clazz).zipWithIndex.flatMap { case (v, index) => 92 | val commaPrefix = if (index > 0) ", " else "" 93 | 94 | val name = paramName(v) 95 | 96 | List( 97 | literalConstant(commaPrefix ++ name.toString ++ " = "), 98 | selectInThis(clazz, name) 99 | ) 100 | } 101 | 102 | val paramParts = 103 | if (api.isObject(clazz)) Nil 104 | else 105 | List( 106 | List(literalConstant("(")), 107 | paramListParts, 108 | List(literalConstant(")")) 109 | ).flatten 110 | 111 | val parts = 112 | namePart :: paramParts 113 | 114 | parts.reduceLeft(concat(_, _)) 115 | } 116 | 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /project/BackpublishPlugin.scala: -------------------------------------------------------------------------------- 1 | import sbt.Keys._ 2 | 3 | import sbt.AutoPlugin 4 | import sbt.Def 5 | import sbt.ThisBuild 6 | 7 | /** Allows publishing an existing version for a new version of Scala. Usage: 8 | * {{{ 9 | * BACKPUBLISH_VERSION=2.12.17 PROJECT_VERSION=0.3.17 sbt clean tlRelease 10 | * }}} 11 | */ 12 | object BackpublishPlugin extends AutoPlugin { 13 | 14 | override def projectSettings: Seq[Def.Setting[_]] = 15 | Option(System.getenv("BACKPUBLISH_VERSION")).toList.flatMap { backpublishVersion => 16 | val projectVersion = Option(System.getenv("PROJECT_VERSION")) 17 | .getOrElse(sys.error("No PROJECT_VERSION provided")) 18 | 19 | println( 20 | s"Going to backpublish artifacts for Scala version $backpublishVersion, project version $projectVersion" 21 | ) 22 | 23 | Seq( 24 | ThisBuild / version := projectVersion, 25 | ThisBuild / scalaVersion := backpublishVersion, 26 | ThisBuild / isSnapshot := false, 27 | ThisBuild / crossScalaVersions := Seq(backpublishVersion) 28 | ) 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /project/ReadmePlugin.scala: -------------------------------------------------------------------------------- 1 | import sbt.AutoPlugin 2 | 3 | import sbt.Def 4 | import sbt._ 5 | import sbt.Keys.crossScalaVersions 6 | 7 | object ReadmePlugin extends AutoPlugin { 8 | 9 | val readmeGenerate = taskKey[String]("Generate README.md") 10 | val readmeWrite = taskKey[Unit]("Write README.md") 11 | val readmeCheck = taskKey[Unit]("Check the contents of README.md") 12 | 13 | override def projectSettings: Seq[Def.Setting[_]] = Seq( 14 | readmeGenerate := Def.task { 15 | val template = IO.read(file("README.md")) 16 | 17 | def pattern(inside: String) = s"""$inside""" 18 | 19 | val versionsGrouped = crossScalaVersions.value.groupBy { v => 20 | v.split("\\.").take(2).mkString(".") 21 | } 22 | 23 | val versionsString = versionsGrouped 24 | .keys 25 | .toSeq 26 | .sorted 27 | .map { prefix => 28 | "- " + versionsGrouped(prefix).mkString(", ") 29 | } 30 | .mkString(s"The plugin is currently published for the following ${crossScalaVersions.value.size} Scala versions:\n\n", "\n", "") 31 | 32 | pattern("(?s)(.+)").r.replaceAllIn(template, pattern(s"\n$versionsString\n")) 33 | }.value, 34 | readmeWrite := Def.task { 35 | IO.write(file("README.md"), readmeGenerate.value) 36 | }.value, 37 | readmeCheck := Def.task { 38 | val expected = IO.read(file("README.md")).trim 39 | val actual = readmeGenerate.value.trim 40 | if (expected != actual) 41 | sys.error(s"""README.md mismatch! Expected: 42 | |$expected 43 | |Actual: 44 | |$actual 45 | | 46 | |Run `sbt readmeWrite` and commit the result to try again.""".stripMargin) 47 | }.value 48 | ) 49 | 50 | } 51 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.11.1 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.8.0") 2 | addSbtPlugin("org.typelevel" % "sbt-typelevel-mergify" % "0.8.0") 3 | addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.1") 4 | -------------------------------------------------------------------------------- /scala-versions: -------------------------------------------------------------------------------- 1 | 2.12.18 2 | 2.12.19 3 | 2.12.20 4 | 2.13.12 5 | 2.13.13 6 | 2.13.14 7 | 2.13.15 8 | 2.13.16 9 | 3.3.0 10 | 3.3.1 11 | 3.3.3 12 | 3.3.4 13 | 3.3.5 14 | 3.3.6 15 | 3.4.0 16 | 3.4.1 17 | 3.4.2 18 | 3.4.3 19 | 3.5.0 20 | 3.5.1 21 | 3.5.2 22 | 3.6.2 23 | 3.6.3 24 | 3.6.4 25 | 3.7.0 26 | 3.7.1 27 | -------------------------------------------------------------------------------- /tests/src/test/scala-3/Scala3Tests.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Polyvariant 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import munit.FunSuite 18 | 19 | class Scala3Tests extends FunSuite: 20 | test("an enum made of constants should have a normal toString") { 21 | assertEquals( 22 | ScalaVersion.Scala2.toString, 23 | // https://github.com/polyvariant/better-tostring/issues/60 24 | // should be "ScalaVersion.Scala2" 25 | "Scala2" 26 | ) 27 | assertEquals( 28 | ScalaVersion.Scala3.toString, 29 | // https://github.com/polyvariant/better-tostring/issues/60 30 | // should be "ScalaVersion.Scala3" 31 | "Scala3" 32 | ) 33 | } 34 | 35 | test("an enum being an ADT should get a custom toString") { 36 | assertEquals( 37 | User.LoggedIn("admin").toString, 38 | "User.LoggedIn(name = admin)" 39 | ) 40 | assertEquals( 41 | User.Unauthorized.toString, 42 | // https://github.com/polyvariant/better-tostring/issues/60 43 | // should be "User.Unauthorized" 44 | "Unauthorized" 45 | ) 46 | } 47 | 48 | test("an enum with a custom toString should use it") { 49 | assertEquals( 50 | EnumCustomTostring.SimpleCase.toString, 51 | "example" 52 | ) 53 | 54 | // https://github.com/polyvariant/better-tostring/issues/34 55 | // we aren't there yet - need to be able to find inherited `toString`s first 56 | assertEquals( 57 | EnumCustomTostring.ParameterizedCase("foo").toString, 58 | // Should be "example" because the existing toString should take precedence. 59 | // Update the test when #34 is fixed. 60 | "EnumCustomTostring.ParameterizedCase(value = foo)" 61 | ) 62 | } 63 | 64 | enum ScalaVersion: 65 | case Scala2, Scala3 66 | 67 | enum EnumCustomTostring: 68 | case SimpleCase 69 | case ParameterizedCase(value: String) 70 | override def toString: String = "example" 71 | 72 | enum User: 73 | case LoggedIn(name: String) 74 | case Unauthorized 75 | -------------------------------------------------------------------------------- /tests/src/test/scala/Tests.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Polyvariant 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import munit.FunSuite 18 | 19 | class Tests extends FunSuite { 20 | 21 | test("Simple case class stringifies nicely") { 22 | assertEquals( 23 | SimpleCaseClass( 24 | "Joe", 25 | 23 26 | ).toString, 27 | "SimpleCaseClass(name = Joe, age = 23)" 28 | ) 29 | } 30 | 31 | test("Case class with multiple parameter lists only has the first list included") { 32 | assertEquals( 33 | MultiParameterList("foo", 20)( 34 | "s" 35 | ).toString, 36 | "MultiParameterList(name = foo, age = 20)" 37 | ) 38 | } 39 | 40 | test("Case class with custom toString should not be overridden") { 41 | assertEquals( 42 | CustomTostring("Joe").toString, 43 | "***" 44 | ) 45 | } 46 | test("Case class with custom toString val should not be overridden") { 47 | assertEquals( 48 | CustomTostringVal("Joe").toString, 49 | "***" 50 | ) 51 | } 52 | 53 | test("Method with alternate constructors should stringify based on primary constructor") { 54 | assertEquals( 55 | new HasOtherConstructors( 56 | 10 57 | ).toString, 58 | "HasOtherConstructors(s = 10 beers)" 59 | ) 60 | } 61 | 62 | test("Case class nested in an object should include enclosing object's name") { 63 | assertEquals( 64 | ObjectNestedParent.ObjectNestedClass("Joe").toString, 65 | "ObjectNestedParent.ObjectNestedClass(name = Joe)" 66 | ) 67 | } 68 | 69 | test("Case object nested in an object should include enclosing object's name") { 70 | assertEquals( 71 | ObjectNestedParent.ObjectNestedObject.toString, 72 | "ObjectNestedParent.ObjectNestedObject" 73 | ) 74 | } 75 | 76 | test("Class nested in a package object should not include package's name") { 77 | assertEquals( 78 | pack.InPackageObject("Joe").toString, 79 | "InPackageObject(name = Joe)" 80 | ) 81 | } 82 | 83 | test("Class nested in another class should stringify normally") { 84 | assertEquals( 85 | new NestedParent().NestedChild("a").toString, 86 | "NestedChild(a)" 87 | ) 88 | } 89 | 90 | test("Class nested in an object itself nested in a class should stringify normally") { 91 | assertEquals( 92 | new DeeplyNestedInClassGrandparent() 93 | .DeeplyNestedInClassParent 94 | .DeeplyNestedInClassClass("a") 95 | .toString, 96 | "DeeplyNestedInClassClass(a)" 97 | ) 98 | } 99 | 100 | test("Method-local class should stringify normally") { 101 | assertEquals( 102 | MethodLocalWrapper.methodLocalClassStringify, 103 | "LocalClass(a)" 104 | ) 105 | } 106 | 107 | test("Lone case object should use the default toString") { 108 | assertEquals(CaseObject.toString, "CaseObject") 109 | } 110 | 111 | test("Case object with toString should not get extra toString") { 112 | assertEquals( 113 | CaseObjectWithToString.toString, 114 | "example" 115 | ) 116 | } 117 | 118 | test("Case object with toString val should not get extra toString") { 119 | assertEquals( 120 | CaseObjectWithToStringVal.toString, 121 | "example" 122 | ) 123 | } 124 | 125 | // https://github.com/polyvariant/better-tostring/issues/34 126 | test("(FAIL) Case class with inherited toString should not get extra toString".fail) { 127 | assertEquals( 128 | HasInheritedToString(0).toString, 129 | "defined in superclass" 130 | ) 131 | } 132 | 133 | test("Case class with inherited and overridden toString should use the override") { 134 | assertEquals( 135 | HasInheritedAndCustomToString(0).toString, 136 | "defined in child" 137 | ) 138 | } 139 | 140 | } 141 | 142 | case object CaseObject 143 | 144 | case object CaseObjectWithToString { 145 | override def toString: String = "example" 146 | } 147 | 148 | case object CaseObjectWithToStringVal { 149 | override val toString: String = "example" 150 | } 151 | 152 | final case class SimpleCaseClass(name: String, age: Int) 153 | final case class MultiParameterList(name: String, age: Int)(val s: String) 154 | 155 | final case class CustomTostring(name: String) { 156 | override def toString: String = "***" 157 | } 158 | 159 | final case class CustomTostringVal(name: String) { 160 | override val toString: String = "***" 161 | } 162 | 163 | final case class HasOtherConstructors(s: String) { 164 | def this(a: Int) = this(a.toString + " beers") 165 | } 166 | 167 | final class NestedParent() { 168 | case class NestedChild(name: String) 169 | } 170 | 171 | object ObjectNestedParent { 172 | case class ObjectNestedClass(name: String) 173 | case object ObjectNestedObject 174 | } 175 | 176 | final class DeeplyNestedInClassGrandparent { 177 | 178 | object DeeplyNestedInClassParent { 179 | case class DeeplyNestedInClassClass(name: String) 180 | } 181 | 182 | } 183 | 184 | object MethodLocalWrapper { 185 | 186 | def methodLocalClassStringify: String = { 187 | final case class LocalClass(name: String) 188 | 189 | LocalClass("a").toString() 190 | } 191 | 192 | } 193 | 194 | trait HasToString { 195 | override def toString(): String = "defined in superclass" 196 | } 197 | 198 | case class HasInheritedToString(i: Int) extends HasToString 199 | 200 | case class HasInheritedAndCustomToString(i: Int) extends HasToString { 201 | override def toString(): String = "defined in child" 202 | } 203 | -------------------------------------------------------------------------------- /tests/src/test/scala/pack/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Polyvariant 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package object pack { 18 | case class InPackageObject(name: String) 19 | } 20 | --------------------------------------------------------------------------------