├── .github └── workflows │ ├── ci.yml │ ├── dependencies.yml │ └── release.yml ├── .gitignore ├── .scalafix.conf ├── .scalafmt.conf ├── LICENSE ├── NOTICE.md ├── README-en.md ├── README.md ├── build.sbt ├── input └── src │ └── main │ ├── scala-2.13 │ └── fix │ │ └── pixiv │ │ ├── MockitoThenToDo.scala │ │ ├── ScalatestAssertThrowsToIntercept.scala │ │ └── UnifyEmptyList.scala │ └── scala │ └── fix │ └── pixiv │ ├── CheckIsEmpty.scala │ ├── NamingConventionPackage.scala │ ├── NeedMessageExtendsRuntimeException.scala │ ├── NonCaseException.scala │ ├── PackageJavaxToJakarta.scala │ ├── SingleConditionMatch.scala │ ├── UnifiedArrow.scala │ ├── UnnecessarySemicolon.scala │ └── ZeroIndexToHead.scala ├── output └── src │ └── main │ ├── scala-2.13 │ └── fix │ │ └── pixiv │ │ ├── MockitoThenToDo.scala │ │ ├── ScalatestAssertThrowsToIntercept.scala │ │ └── UnifyEmptyList.scala │ └── scala │ └── fix │ └── pixiv │ ├── CheckIsEmpty.scala │ ├── NeedMessageExtendsRuntimeException.scala │ ├── NonCaseException.scala │ ├── PackageJavaxToJakarta.scala │ ├── SingleConditionMatch.scala │ ├── UnifiedArrow.scala │ ├── UnnecessarySemicolon.scala │ └── ZeroIndexToHead.scala ├── project ├── TargetAxis.scala ├── build.properties └── plugins.sbt ├── rules └── src │ ├── main │ ├── resources │ │ └── META-INF │ │ │ └── services │ │ │ └── scalafix.v1.Rule │ └── scala │ │ ├── fix │ │ └── pixiv │ │ │ ├── CheckIsEmpty.scala │ │ │ ├── CheckIsEmptyConfig.scala │ │ │ ├── LogConfig.scala │ │ │ ├── LogLevel.scala │ │ │ ├── MockitoThenToDo.scala │ │ │ ├── NamingConventionPackage.scala │ │ │ ├── NamingConventionPackageConfig.scala │ │ │ ├── NeedMessageExtendsRuntimeException.scala │ │ │ ├── NonCaseException.scala │ │ │ ├── PackageJavaxToJakarta.scala │ │ │ ├── ScalatestAssertThrowsToIntercept.scala │ │ │ ├── SingleConditionMatch.scala │ │ │ ├── UnifiedArrow.scala │ │ │ ├── UnifyEmptyList.scala │ │ │ ├── UnnecessarySemicolon.scala │ │ │ └── ZeroIndexToHead.scala │ │ └── util │ │ ├── SemanticTypeConverter.scala │ │ ├── SymbolConverter.scala │ │ ├── ToClassException.scala │ │ └── TreeUtil.scala │ └── test │ └── scala │ └── util │ └── SemanticTypeConverterTest.scala └── tests └── src └── test └── scala └── fix └── RuleSuite.scala /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | jobs: 8 | checks: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - uses: olafurpg/setup-scala@v13 13 | - name: scalafix 14 | run: sbt "+src/scalafix --check" 15 | - name: scalafmt 16 | run: sbt scalafmtSbtCheck "src/scalafmtCheckAll" 17 | - name: Test 18 | run: sbt test 19 | -------------------------------------------------------------------------------- /.github/workflows/dependencies.yml: -------------------------------------------------------------------------------- 1 | name: dependencies 2 | on: 3 | workflow_dispatch: 4 | schedule: 5 | - cron: '0 22 * * *' 6 | jobs: 7 | scala-steward: 8 | runs-on: ubuntu-latest 9 | name: Launch Scala Steward 10 | steps: 11 | - name: Scala Steward Github Action 12 | uses: scala-steward-org/scala-steward-action@v2.29.0 13 | with: 14 | github-token: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | tags: ["*"] 5 | jobs: 6 | publish: 7 | runs-on: ubuntu-20.04 8 | steps: 9 | - uses: actions/checkout@v2.3.4 10 | with: 11 | fetch-depth: 0 12 | - uses: coursier/cache-action@v6.3 13 | - uses: coursier/setup-action@v1.3.3 14 | with: 15 | jvm: adopt:11 16 | - run: sbt ci-release 17 | env: 18 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 19 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 20 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 21 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea 3 | .bsp 4 | dist/* 5 | target/ 6 | *.class 7 | *.log -------------------------------------------------------------------------------- /.scalafix.conf: -------------------------------------------------------------------------------- 1 | rules = [ 2 | RemoveUnused 3 | NoAutoTupling 4 | NoValInForComprehension 5 | ProcedureSyntax 6 | ExplicitResultTypes 7 | OrganizeImports 8 | fix.scala213.FinalObject 9 | fix.scala213.Any2StringAdd 10 | fix.scala213.ExplicitNullaryEtaExpansion 11 | fix.scala213.Varargs 12 | ] 13 | 14 | RemoveUnused { 15 | imports = true 16 | privates = false 17 | locals = true 18 | patternvars = true 19 | params = false 20 | } 21 | 22 | OrganizeImports { 23 | coalesceToWildcardImportThreshold = 6 24 | groupedImports = Merge 25 | groups = [ 26 | "re:javax?\\." 27 | "scala.", 28 | "*", 29 | "com.sun." 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = "3.3.1" 2 | style = default 3 | maxColumn = 120 4 | align.openParenCallSite = false 5 | align.tokens = [] 6 | importSelectors = singleLine 7 | docstrings.style = SpaceAsterisk 8 | docstrings.blankFirstLine = yes 9 | docstrings.wrap = no 10 | newlines.source = keep 11 | newlines.beforeMultiline = keep 12 | newlines.forceBeforeMultilineAssign = never 13 | newlines.implicitParamListModifierPrefer = before 14 | newlines.avoidForSimpleOverflow = [slc] 15 | danglingParentheses.ctrlSite = false 16 | runner.dialect = scala213source3 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /NOTICE.md: -------------------------------------------------------------------------------- 1 | # NOTICE 2 | 3 | | Category | License | Dependency | Notes | 4 | |--------------|--------------------------------------------------------------------------------------------|---------------------------------------------------------------|-------------------------| 5 | | Apache | [Apache 2](http://www.apache.org/licenses/LICENSE-2.0.txt) | com.thesamet.scalapb # lenses_2.13 # 0.11.4 | | 6 | | Apache | [Apache 2](http://www.apache.org/licenses/LICENSE-2.0.txt) | com.thesamet.scalapb # scalapb-runtime_2.13 # 0.11.4 | | 7 | | Apache | [Apache License v2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | net.java.dev.jna # jna # 5.8.0 | | 8 | | Apache | [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0) | ch.epfl.scala # scalafix-core_2.13 # 0.9.33 | | 9 | | Apache | [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0) | com.geirsson # metaconfig-core_2.13 # 0.9.15 | | 10 | | Apache | [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0) | com.geirsson # metaconfig-typesafe-config_2.13 # 0.9.15 | | 11 | | Apache | [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) | com.typesafe # config # 1.4.1 | | 12 | | Apache | [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) | org.scala-lang # scala-compiler # 2.13.7 | | 13 | | Apache | [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) | org.scala-lang # scala-library # 2.13.7 | | 14 | | Apache | [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) | org.scala-lang # scala-reflect # 2.13.7 | | 15 | | Apache | [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) | org.scala-lang # scalap # 2.13.7 | | 16 | | Apache | [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) | org.scala-lang.modules # scala-collection-compat_2.13 # 2.6.0 | | 17 | | Apache | [Apache-2.0](https://www.apache.org/licenses/LICENSE-2.0) | org.scala-lang.modules # scala-xml_2.13 # 2.0.1 | | 18 | | Apache | [Apache-2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.typelevel # paiges-core_2.13 # 0.4.2 | | 19 | | Apache | [The Apache Software License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0.txt) | com.googlecode.java-diff-utils # diffutils # 1.3.0 | | 20 | | Apache | [the Apache License, ASL Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.scalactic # scalactic_2.13 # 3.2.11 | | 21 | | Apache | [the Apache License, ASL Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.scalatest # scalatest-compatible # 3.2.11 | | 22 | | Apache | [the Apache License, ASL Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.scalatest # scalatest-core_2.13 # 3.2.11 | | 23 | | Apache | [the Apache License, ASL Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.scalatest # scalatest-diagrams_2.13 # 3.2.11 | | 24 | | Apache | [the Apache License, ASL Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.scalatest # scalatest-featurespec_2.13 # 3.2.11 | | 25 | | Apache | [the Apache License, ASL Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.scalatest # scalatest-flatspec_2.13 # 3.2.11 | | 26 | | Apache | [the Apache License, ASL Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.scalatest # scalatest-freespec_2.13 # 3.2.11 | | 27 | | Apache | [the Apache License, ASL Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.scalatest # scalatest-funspec_2.13 # 3.2.11 | | 28 | | Apache | [the Apache License, ASL Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.scalatest # scalatest-funsuite_2.13 # 3.2.11 | | 29 | | Apache | [the Apache License, ASL Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.scalatest # scalatest-matchers-core_2.13 # 3.2.11 | | 30 | | Apache | [the Apache License, ASL Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.scalatest # scalatest-mustmatchers_2.13 # 3.2.11 | | 31 | | Apache | [the Apache License, ASL Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.scalatest # scalatest-propspec_2.13 # 3.2.11 | | 32 | | Apache | [the Apache License, ASL Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.scalatest # scalatest-refspec_2.13 # 3.2.11 | | 33 | | Apache | [the Apache License, ASL Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.scalatest # scalatest-shouldmatchers_2.13 # 3.2.11 | | 34 | | Apache | [the Apache License, ASL Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.scalatest # scalatest-wordspec_2.13 # 3.2.11 | | 35 | | Apache | [the Apache License, ASL Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) | org.scalatest # scalatest_2.13 # 3.2.11 | | 36 | | BSD | [3-Clause BSD License](https://opensource.org/licenses/BSD-3-Clause) | com.google.protobuf # protobuf-java # 3.15.8 | | 37 | | BSD | [BSD](https://github.com/scalameta/scalameta/blob/main/LICENSE.md) | org.scalameta # common_2.13 # 4.4.30 | | 38 | | BSD | [BSD](https://github.com/scalameta/scalameta/blob/main/LICENSE.md) | org.scalameta # parsers_2.13 # 4.4.30 | | 39 | | BSD | [BSD](https://github.com/scalameta/scalameta/blob/main/LICENSE.md) | org.scalameta # scalameta_2.13 # 4.4.30 | | 40 | | BSD | [BSD](https://github.com/scalameta/scalameta/blob/main/LICENSE.md) | org.scalameta # trees_2.13 # 4.4.30 | | 41 | | MIT | [MIT](https://spdx.org/licenses/MIT.html) | com.lihaoyi # fansi_2.13 # 0.2.14 | | 42 | | MIT | [MIT](https://spdx.org/licenses/MIT.html) | com.lihaoyi # geny_2.13 # 0.6.5 | | 43 | | MIT | [MIT](https://spdx.org/licenses/MIT.html) | com.lihaoyi # pprint_2.13 # 0.6.6 | | 44 | | MIT | [MIT](https://spdx.org/licenses/MIT.html) | com.lihaoyi # sourcecode_2.13 # 0.2.7 | | 45 | | MIT | [MIT](https://spdx.org/licenses/MIT.html) | org.scalameta # fastparse-v2_2.13 # 2.3.1 | | 46 | | unrecognized | [none specified](none specified) | org.jline # jline # 3.20.0 | | 47 | 48 | -------------------------------------------------------------------------------- /README-en.md: -------------------------------------------------------------------------------- 1 | ![Maven Central](https://maven-badges.herokuapp.com/maven-central/net.pixiv/scalafix-pixiv-rule_2.13/badge.svg) 2 | 3 | # Scalafix rules for Scalafix-Rule-Pixiv 4 | 5 | General refactoring rules available in [scalafix](https://scalacenter.github.io/scalafix/) 6 | 7 | 日本語版の README.md は [こちら](./README.md) 8 | 9 | ## Install 10 | 11 | ```sbt 12 | ThisBuild / scalafixDependencies += "net.pixiv" %% "scalafix-pixiv-rule" % "" 13 | ``` 14 | 15 | ## fix.pixiv.UnnecessarySemicolon 16 | 17 | Remove unnecessary end-of-line semicolons. If a line is connected after the semicolon, it will be not deleted. 18 | 19 | ```scala 20 | /* rule = UnnecessarySemicolon */ 21 | val x = 3; // rewrite to: `val x = 3` 22 | val a = 1; val b = 2 23 | ``` 24 | 25 | ## fix.pixiv.UnifiedArrow 26 | 27 | Replace deprecated arrow (→, ⇒) characters with ->, =>. 28 | 29 | ```scala 30 | /* rule = UnifiedArrow */ 31 | List(1 → "a", 2 → "b", 3 → "c").map { // rewrite to: List(1 -> "a", 2 -> "b", 3 -> "c").map { 32 | case (_, s) ⇒ s // rewrite to: case (_, s) => s 33 | } 34 | ``` 35 | 36 | ## fix.pixiv.ZeroIndexToHead 37 | 38 | Replaces access to the first element by index in `Seq` with a call to `head`. 39 | 40 | ```scala 41 | /* rule = ZeroIndexToHead */ 42 | Seq(1, 2, 3)(0) // rewrite to: `Seq(1, 2, 3).head` 43 | ``` 44 | 45 | ## fix.pixiv.CheckIsEmpty 46 | 47 | Replace emptiness checks for `Option` and `Seq` with `isEmpty`, `nonEmpty`, and `isDefined`. 48 | 49 | ```scala 50 | /* rule = CheckIsEmpty */ 51 | Some(1) == None // rewrite to: Some(1).isEmpty 52 | Some(1).nonEmpty // if `CheckIsEmpty.alignIsDefined = true` then rewrite to Some(1).isDefined 53 | ``` 54 | 55 | ## fix.pixiv.NonCaseException 56 | 57 | Raise a warning for `case class` definitions that inherit from `Exception`. 58 | 59 | This is because implementing `case class` would compromise the benefits of exception hierarchy and uniqueness. 60 | 61 | ```scala 62 | /* rule = NonCaseException */ 63 | case class NonCaseException(msg: String) extends RuntimeException(msg) 64 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 65 | case class として Exception を継承することは推奨されません: NonCaseException 66 | ``` 67 | 68 | ## fix.pixiv.UnifyEmptyList 69 | 70 | ⚠️ Only Scala 2.13.x is supported 71 | 72 | Replace `List()` and `List.empty` without type variables specification with `Nil`. 73 | 74 | This is because `Nil` is defined as `List[Nothing]`. 75 | 76 | Also, `List[Any]()` with a type variable specification is replaced with `List.empty[Any]`. 77 | 78 | ```scala 79 | /* rule = EmptyListToNil */ 80 | val empty = List.empty // rewrite to: val empty = Nil 81 | val list = List[String]() // rewrite to: val list = List.empty[String] 82 | ``` 83 | 84 | ## fix.pixiv.SingleConditionMatch 85 | 86 | Split a pattern match that has only one `case`. 87 | 88 | It also replaces patterns where only `Some.unapply` is used with `foreach` calls. 89 | 90 | ⚠︎This rule may destroy indentation. Always use with a formatter! 91 | 92 | ```scala 93 | /* rule = SingleConditionMatch */ 94 | Some(1) match { 95 | case result => println(result) 96 | } 97 | 98 | /* rewrite to: 99 | * println(Some(1)) 100 | */ 101 | ``` 102 | 103 | Due to technical issues, only the most deeply nested patterns will be replaced. 104 | 105 | ```scala 106 | /* rule = SingleConditionMatch */ 107 | Some(1) match { 108 | case result => 109 | result match { 110 | case result => println(result) 111 | } 112 | } 113 | 114 | /* rewrite to: 115 | * Some(1) match { 116 | * case result => 117 | * println(result) 118 | * } 119 | */ 120 | ``` 121 | 122 | 123 | ## fix.pixiv.MockitoThenToDo 124 | 125 | Unify the notation for using [Mockito](https://site.mockito.org/) in Scala. 126 | 127 | While when/then syntax can be used to check the return type, it has some disadvantages, such as not being able to return void (Unit). 128 | This rule replaces test code written in when/then syntax with do/when syntax. 129 | 130 | ```scala 131 | /* rule = MockitoThenToDo */ 132 | Mockito.when(a.hoge()).thenReturn("mock1").thenReturn("mock2") 133 | when(a.fuga).thenReturn("mock") 134 | 135 | /* rewrite to: 136 | * Mockito.doReturn("mock1").doReturn("mock2").when(a).hoge() 137 | * Mockito.doReturn("mock").when(a).fuga 138 | */ 139 | ``` 140 | 141 | ## fix.pixiv.ScalatestAssertThrowsToIntercept 142 | Unify the assertion of Exceptions for using [Scalatest](https://www.scalatest.org/). 143 | 144 | In the case of Exception assertion in article [using_assertions](https://www.scalatest.org/user_guide/using_assertions), there are `assertThrows` and `intercept` that difference between `assertThrows` and `intercept` is `intercept` returns Exceptions but `assertThrows` does not. 145 | Applying this rule changes `assertThrows` to `intercept`. 146 | ```scala 147 | /* rule = ScalatestAssertThrowsToIntercept */ 148 | assertThrows[RuntimeException]{ 149 | a.hoge() 150 | } 151 | 152 | /* rewrite to: 153 | * intercept[RuntimeException]{ 154 | * a.hoge() 155 | * } 156 | */ 157 | ``` 158 | 159 | ## fix.pixiv.NeedMessageExtendsRuntimeException 160 | 161 | Raise a warning if a message is not given of a `class` that extends `RuntimeException`. 162 | This is to allow identification when errors are notified. 163 | 164 | ```scala 165 | /* rule = NeedMessageExtendsRuntimeException */ 166 | class RedException extends RuntimeException {} 167 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 168 | RuntimeException を継承したクラスを作る際にはメッセージを付与してください: RedException 169 | */ 170 | ``` 171 | 172 | ## fix.pixiv.NamingConventionPackage 173 | Generates a warning when a class that follows a certain naming convention is found in an inappropriate package. 174 | 175 | For example, when using the [Play Framework](https://www.playframework.com/documentation/2.8.x/Anatomy), 176 | a class named Controller is required to not be in the filters directory. 177 | 178 | ```scala 179 | /* 180 | rule = NamingConventionPackage 181 | NamingConventionPackage.convention = [ 182 | { package = "^filters$", class = "^.+Controller$" } 183 | ] 184 | */ 185 | package filters 186 | 187 | // class HomeController は filters パッケージに実装すべきではありません 188 | class HomeController {} 189 | ``` 190 | 191 | ## fix.pixiv.PackageJavaxToJakarta 192 | Rewriting rules for namespaces changed since Jakarta EE 9. 193 | The `javax.*` namespace is changed to `jakarta.*`. 194 | 195 | The complete list of packages to be changed is [here](https://github.com/jakartaee/jakartaee-platform/blob/main/namespace/mappings.adoc). 196 | 197 | ```scala 198 | /* rule = PackageJavaxToJakarta */ 199 | package fix.pixiv 200 | 201 | import javax.inject.Inject // rewrite to: import jakarta.inject.Inject 202 | 203 | class PackageJavaxToJakarta @Inject()() { 204 | } 205 | ``` 206 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Maven Central](https://maven-badges.herokuapp.com/maven-central/net.pixiv/scalafix-pixiv-rule_2.13/badge.svg) 2 | 3 | # Scalafix rules for Scalafix-Rule-Pixiv 4 | 5 | [scalafix](https://scalacenter.github.io/scalafix/) で利用できる汎用的なリファクタリングルール 6 | 7 | If you would like the README written in English, please click [here](./README-en.md). 8 | 9 | ## インストール 10 | 11 | ```sbt 12 | ThisBuild / scalafixDependencies += "net.pixiv" %% "scalafix-pixiv-rule" % "" 13 | ``` 14 | 15 | ## fix.pixiv.UnnecessarySemicolon 16 | 17 | 不要な行末セミコロンを削除します。セミコロン後に行が接続されている場合には削除しません。 18 | 19 | ```scala 20 | /* rule = UnnecessarySemicolon */ 21 | val x = 3; // rewrite to: `val x = 3` 22 | val a = 1; val b = 2 23 | ``` 24 | 25 | ## fix.pixiv.UnifiedArrow 26 | 27 | 非推奨の矢印 (→, ⇒) 文字を ->, => に置換します。 28 | 29 | ```scala 30 | /* rule = UnifiedArrow */ 31 | List(1 → "a", 2 → "b", 3 → "c").map { // rewrite to: List(1 -> "a", 2 -> "b", 3 -> "c").map { 32 | case (_, s) ⇒ s // rewrite to: case (_, s) => s 33 | } 34 | ``` 35 | 36 | ## fix.pixiv.ZeroIndexToHead 37 | 38 | `Seq` のインデックスによる最初の要素へのアクセスを `head` 呼び出しに置換します。 39 | 40 | ```scala 41 | /* rule = ZeroIndexToHead */ 42 | Seq(1, 2, 3)(0) // rewrite to: `Seq(1, 2, 3).head` 43 | ``` 44 | 45 | ## fix.pixiv.CheckIsEmpty 46 | 47 | `Option` や `Seq` の空チェックに `isEmpty`, `nonEmpty`, `isDefined` を利用するように置き換えます。 48 | 49 | ```scala 50 | /* rule = CheckIsEmpty */ 51 | Some(1) == None // rewrite to: Some(1).isEmpty 52 | Some(1).nonEmpty // if `CheckIsEmpty.alignIsDefined = true` then rewrite to Some(1).isDefined 53 | ``` 54 | 55 | ## fix.pixiv.NonCaseException 56 | 57 | `Exception` を継承した `case class` の定義に警告を発生させます。 58 | これは、 `case class` として実装することにより、例外の階層化や一意性による恩恵が損なわれるためです。 59 | 60 | ```scala 61 | /* rule = NonCaseException */ 62 | case class NonCaseException(msg: String) extends RuntimeException(msg) 63 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 64 | case class として Exception を継承することは推奨されません: NonCaseException 65 | ``` 66 | 67 | ## fix.pixiv.UnifyEmptyList 68 | 69 | ⚠️ Scala 2.13.x 系のみ対応しています。 70 | 71 | 型変数指定のない `List()` や `List.empty` を `Nil` に置き換えます。 72 | これは、 `Nil` が `List[Nothing]` として定義されているためです。 73 | また、型変数指定のある `List[Any]()` は `List.empty[Any]` へと置換されます。 74 | 75 | ```scala 76 | /* rule = EmptyListToNil */ 77 | val empty = List.empty // rewrite to: val empty = Nil 78 | val list = List[String]() // rewrite to: val list = List.empty[String] 79 | ``` 80 | 81 | ## fix.pixiv.SingleConditionMatch 82 | 83 | 単一の `case` しか持たないパターンマッチを分解します。 84 | 85 | また、 `Some.unapply` のみが利用されているパターンを `foreach` 呼び出しに置換します。 86 | 87 | ※このルールではインデントが破壊されることがあります。必ずフォーマッターと併用してください 88 | 89 | ```scala 90 | /* rule = SingleConditionMatch */ 91 | Some(1) match { 92 | case result => println(result) 93 | } 94 | 95 | /* rewrite to: 96 | * println(Some(1)) 97 | */ 98 | ``` 99 | 100 | 技術的な問題により、ネストされたパターンでは、最深のものだけが置換されます。 101 | 102 | ```scala 103 | /* rule = SingleConditionMatch */ 104 | Some(1) match { 105 | case result => 106 | result match { 107 | case result => println(result) 108 | } 109 | } 110 | 111 | /* rewrite to: 112 | * Some(1) match { 113 | * case result => 114 | * println(result) 115 | * } 116 | */ 117 | ``` 118 | 119 | ## fix.pixiv.MockitoThenToDo 120 | 121 | [Mockito](https://site.mockito.org/) を Scala で使う場合の記法を統一します。 122 | 123 | when/then 構文は戻り値型のチェックを受けられる反面で、 void (Unit) を返却したい場合は使えないなどのデメリットがあります。 124 | このルールでは、 when/then 構文で書かれたテストコードを do/when 構文に置き換えます。 125 | 126 | ```scala 127 | /* rule = MockitoThenToDo */ 128 | Mockito.when(a.hoge()).thenReturn("mock1").thenReturn("mock2") 129 | when(a.fuga).thenReturn("mock") 130 | 131 | /* rewrite to: 132 | * Mockito.doReturn("mock1").doReturn("mock2").when(a).hoge() 133 | * Mockito.doReturn("mock").when(a).fuga 134 | */ 135 | ``` 136 | 137 | ## fix.pixiv.ScalatestAssertThrowsToIntercept 138 | [Scalatest](https://www.scalatest.org/)のExceptionのassertionを統一します。 139 | 140 | [using_assertions](https://www.scalatest.org/user_guide/using_assertions)のドキュメントで、Exceptionsのassertionの場合には`assertThrows`と`intercept`がありますが、 二つの違いは`intercept`はExceptionsを値として返し、`assertThrows`は返さないという振る舞いをします。 141 | このルールでは`assertThrows`を`intercept`に変更するものです。 142 | ```scala 143 | /* rule = ScalatestAssertThrowsToIntercept */ 144 | assertThrows[RuntimeException]{ 145 | a.hoge() 146 | } 147 | 148 | /* rewrite to: 149 | * intercept[RuntimeException]{ 150 | * a.hoge() 151 | * } 152 | */ 153 | ``` 154 | 155 | ## fix.pixiv.NeedMessageExtendsRuntimeException 156 | 157 | `RuntimeException` を継承した `class` の定義にメッセージが付与されていない場合は警告を発生させます。 158 | これは、エラー通知時の識別を可能にするためです。 159 | 160 | ```scala 161 | /* rule = NeedMessageExtendsRuntimeException */ 162 | class RedException extends RuntimeException {} 163 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 164 | RuntimeException を継承したクラスを作る際にはメッセージを付与してください: RedException 165 | */ 166 | ``` 167 | 168 | ## fix.pixiv.NamingConventionPackage 169 | ある命名規則に倣ったクラスが適切でないパッケージにある場合に警告を発生させます。 170 | 171 | 例えば、 [Play Framework](https://www.playframework.com/documentation/2.8.x/Anatomy) を利用している場合に、 172 | `Controller` と命名されたクラスが `filters` ディレクトリをないことを要求します。 173 | 174 | ```scala 175 | /* 176 | rule = NamingConventionPackage 177 | NamingConventionPackage.convention = [ 178 | { package = "^filters$", class = "^.+Controller$" } 179 | ] 180 | */ 181 | package filters 182 | 183 | // class HomeController は filters パッケージに実装すべきではありません 184 | class HomeController {} 185 | ``` 186 | 187 | ## fix.pixiv.PackageJavaxToJakarta 188 | 189 | Jakarta EE 9 から変更された名前空間に対応した書き換えルール。 190 | `javax.*` 名前空間を `jakarta.*` に変更します。 191 | 192 | 変更されるパッケージの完全なリストは[こちら](https://github.com/jakartaee/jakartaee-platform/blob/main/namespace/mappings.adoc)。 193 | 194 | ```scala 195 | /* rule = PackageJavaxToJakarta */ 196 | package fix.pixiv 197 | 198 | import javax.inject.Inject // rewrite to: import jakarta.inject.Inject 199 | 200 | class PackageJavaxToJakarta @Inject()() { 201 | } 202 | ``` 203 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | lazy val V = _root_.scalafix.sbt.BuildInfo 2 | 3 | lazy val rulesCrossVersions = Seq(V.scala213, V.scala212) 4 | lazy val scala3Version = "3.3.0" 5 | 6 | ThisBuild / scalafixScalaBinaryVersion := CrossVersion.binaryScalaVersion(scalaVersion.value) 7 | ThisBuild / scalafixDependencies += "com.github.liancheng" %% "organize-imports" % "0.6.0" 8 | ThisBuild / scalafixDependencies += "com.sandinh" %% "scala-rewrites" % "1.1.0-M1" 9 | 10 | inThisBuild( 11 | List( 12 | organization := "net.pixiv", 13 | homepage := Some(url("https://github.com/pixiv/scalafix-pixiv-rule")), 14 | licenses := List( 15 | "Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0") 16 | ), 17 | developers := List( 18 | Developer( 19 | "Javakky-pxv", 20 | "Kazuma Iemura", 21 | "javakky@pixiv.co.jp", 22 | url("https://github.com/Javakky-pxv") 23 | ) 24 | ), 25 | semanticdbEnabled := true, 26 | semanticdbVersion := scalafixSemanticdb.revision, 27 | scalaVersion := "2.13.11", 28 | sonatypeCredentialHost := "s01.oss.sonatype.org", 29 | sonatypeRepository := "https://s01.oss.sonatype.org/service/local" 30 | ) 31 | ) 32 | 33 | lazy val `scalafix-rule-pixiv` = (project in file(".")) 34 | .aggregate( 35 | rules.projectRefs ++ 36 | input.projectRefs ++ 37 | output.projectRefs ++ 38 | tests.projectRefs: _* 39 | ) 40 | .settings( 41 | publish / skip := true 42 | ) 43 | 44 | lazy val src = (project in file("rules")) 45 | .settings( 46 | libraryDependencies ++= Seq( 47 | "ch.epfl.scala" %% "scalafix-core" % V.scalafixVersion, 48 | "org.scalatest" %% "scalatest" % "3.2.16" 49 | ), 50 | scalacOptions ++= Seq( 51 | "-deprecation", 52 | "-feature", 53 | "-Ywarn-unused:imports,locals,patvars" 54 | ), 55 | semanticdbEnabled := true, 56 | semanticdbVersion := scalafixSemanticdb.revision, 57 | publish / skip := true, 58 | licenseReportTitle := "NOTICE", 59 | licenseReportDir := `scalafix-rule-pixiv`.base, 60 | licenseReportTypes := Seq(MarkDown) 61 | ) 62 | 63 | lazy val rules = projectMatrix 64 | .settings( 65 | moduleName := "scalafix-pixiv-rule", 66 | libraryDependencies ++= Seq( 67 | "ch.epfl.scala" %% "scalafix-core" % V.scalafixVersion, 68 | "org.scalatest" %% "scalatest" % "3.2.16" 69 | ) 70 | ) 71 | .defaultAxes(VirtualAxis.jvm) 72 | .jvmPlatform(rulesCrossVersions) 73 | 74 | lazy val input = projectMatrix 75 | .settings( 76 | publish / skip := true, 77 | libraryDependencies ++= Seq( 78 | "org.mockito" % "mockito-core" % "5.4.0", 79 | "org.scalatest" %% "scalatest" % "3.2.16", 80 | "javax.inject" % "javax.inject" % "1" 81 | ) 82 | ) 83 | .defaultAxes(VirtualAxis.jvm) 84 | .jvmPlatform(scalaVersions = rulesCrossVersions :+ scala3Version) 85 | 86 | lazy val output = projectMatrix 87 | .settings( 88 | publish / skip := true, 89 | libraryDependencies ++= Seq( 90 | "org.mockito" % "mockito-core" % "5.4.0", 91 | "org.scalatest" %% "scalatest" % "3.2.16", 92 | "jakarta.inject" % "jakarta.inject-api" % "2.0.1" 93 | ) 94 | ) 95 | .defaultAxes(VirtualAxis.jvm) 96 | .jvmPlatform(scalaVersions = rulesCrossVersions :+ scala3Version) 97 | 98 | lazy val testsAggregate = Project("tests", file("target/testsAggregate")) 99 | .aggregate(tests.projectRefs: _*) 100 | 101 | lazy val tests = projectMatrix 102 | .settings( 103 | publish / skip := true, 104 | libraryDependencies += "ch.epfl.scala" % "scalafix-testkit" % V.scalafixVersion % Test cross CrossVersion.full, 105 | scalafixTestkitOutputSourceDirectories := 106 | TargetAxis 107 | .resolve(output, Compile / unmanagedSourceDirectories) 108 | .value, 109 | scalafixTestkitInputSourceDirectories := 110 | TargetAxis 111 | .resolve(input, Compile / unmanagedSourceDirectories) 112 | .value, 113 | scalafixTestkitInputClasspath := 114 | TargetAxis.resolve(input, Compile / fullClasspath).value, 115 | scalafixTestkitInputScalacOptions := 116 | TargetAxis.resolve(input, Compile / scalacOptions).value, 117 | scalafixTestkitInputScalaVersion := 118 | TargetAxis.resolve(input, Compile / scalaVersion).value 119 | ) 120 | .defaultAxes( 121 | rulesCrossVersions.map(VirtualAxis.scalaABIVersion) :+ VirtualAxis.jvm: _* 122 | ) 123 | .customRow( 124 | scalaVersions = Seq(V.scala213), 125 | axisValues = Seq(TargetAxis(V.scala213), VirtualAxis.jvm), 126 | settings = Seq() 127 | ) 128 | .customRow( 129 | scalaVersions = Seq(V.scala212), 130 | axisValues = Seq(TargetAxis(V.scala212), VirtualAxis.jvm), 131 | settings = Seq() 132 | ) 133 | .dependsOn(rules) 134 | .enablePlugins(ScalafixTestkitPlugin) 135 | -------------------------------------------------------------------------------- /input/src/main/scala-2.13/fix/pixiv/MockitoThenToDo.scala: -------------------------------------------------------------------------------- 1 | /* 2 | rule = MockitoThenToDo 3 | */ 4 | package fix.pixiv 5 | 6 | import org.mockito.Mockito.when 7 | import org.mockito.{ArgumentMatchers, Mockito} 8 | 9 | class MockitoThenToDo { 10 | class A { 11 | def hoge(): String = "hoge" 12 | def fuga: String = "fuga" 13 | def piyo(s: String): String = s"piyo: $s" 14 | def foo(a: Int)(b: Int): String = "foo" 15 | } 16 | private val a = Mockito.mock(classOf[A]) 17 | Mockito.when(a.hoge()).thenReturn("mock1").thenReturn("mock2") 18 | when(a.fuga).thenReturn("mock1", "mock2") 19 | Mockito.when(a.piyo(ArgumentMatchers.any())).thenAnswer { invocation => 20 | val s = invocation.getArgument[String](0) 21 | s"mock: $s" 22 | } 23 | Mockito.when(a.foo(1)(2)).thenThrow(new RuntimeException("mock")) 24 | println(a.hoge()) 25 | println(a.fuga) 26 | println(a.piyo("")) 27 | println(a.foo(1)(2)) 28 | } 29 | -------------------------------------------------------------------------------- /input/src/main/scala-2.13/fix/pixiv/ScalatestAssertThrowsToIntercept.scala: -------------------------------------------------------------------------------- 1 | /* 2 | rule = ScalatestAssertThrowsToIntercept 3 | */ 4 | package fix.pixiv 5 | 6 | import org.scalatest.Assertions 7 | import org.scalatest.Assertions._ 8 | 9 | object ScalatestAssertThrowsToIntercept { 10 | Assertions.assertThrows[RuntimeException] { 11 | throw new RuntimeException("assertThrows1") 12 | } 13 | assertThrows[RuntimeException] { 14 | throw new RuntimeException("assertThrows2") 15 | } 16 | // 変更なし 17 | println("assertThrows") 18 | } 19 | -------------------------------------------------------------------------------- /input/src/main/scala-2.13/fix/pixiv/UnifyEmptyList.scala: -------------------------------------------------------------------------------- 1 | /* 2 | rule = UnifyEmptyList 3 | */ 4 | package fix.pixiv 5 | 6 | object UnifyEmptyList { 7 | // 変換する 8 | private val list = List() 9 | private val nothingList = List[Nothing]() 10 | private val empty = List.empty 11 | private val varType: List[String] = List() 12 | List(1, 2).foldLeft[List[Int]](List.empty)((l, a) => l :+ a) 13 | varType match { 14 | case List() => 0 15 | case "1" :: "2" :: Nil => 2 16 | case _ => -1 17 | } 18 | private def func(list: List[String] = List()): Unit = {} 19 | // empty に変換する 20 | private val listType = List[String]() 21 | List(1, 2).foldLeft(List[Int]())((l, a) => l :+ a) 22 | private def typeFunc(list: List[String] = List[String]()): Unit = {} 23 | // 変換しない 24 | private val emptyType = List.empty[String] 25 | List(1, 2).foldLeft(List.empty[Int])((l, a) => l :+ a) 26 | } 27 | -------------------------------------------------------------------------------- /input/src/main/scala/fix/pixiv/CheckIsEmpty.scala: -------------------------------------------------------------------------------- 1 | /* 2 | rule = CheckIsEmpty 3 | CheckIsEmpty.alignIsDefined = true 4 | */ 5 | package fix.pixiv 6 | 7 | object CheckIsEmpty { 8 | private val seq = Seq(1, 2, 3) 9 | private val option = Some(1) 10 | 11 | // to isEmpty 12 | Seq(1, 2, 3).size == 0 13 | seq.size == 0 14 | seq.length == 0 15 | !seq.nonEmpty 16 | 17 | // to nonEmpty 18 | seq.size != 0 19 | seq.size > 0 20 | seq.size >= 1 21 | seq.length != 0 22 | seq.length > 0 23 | seq.length >= 1 24 | seq.exists(_ => true) 25 | seq.exists(Function.const(true)) 26 | seq.size != 0 27 | seq.length != 0 28 | !seq.isEmpty 29 | 30 | // to isEmpty 31 | option.size == 0 32 | option == None 33 | None == option 34 | !option.isDefined 35 | 36 | // to isDefined 37 | option != None 38 | None != option 39 | option.nonEmpty 40 | 41 | // 配列には isEmpty が存在しないので無視する 42 | Array(1, 2, 3).length == 0 43 | 44 | case class InSeq(seq: Seq[Int]) { 45 | def getSeq(i: Int): Seq[Int] = seq.slice(0, i) 46 | } 47 | private val inSeq = InSeq(Seq(1, 2, 3)) 48 | inSeq.seq.size == 0 49 | inSeq.getSeq(2).size == 0 50 | } 51 | -------------------------------------------------------------------------------- /input/src/main/scala/fix/pixiv/NamingConventionPackage.scala: -------------------------------------------------------------------------------- 1 | /* 2 | rule = NamingConventionPackage 3 | NamingConventionPackage.convention = [ 4 | { package = "^.+\\.pixiv$", class = "^.+Package$" } 5 | ] 6 | */ 7 | package fix.pixiv 8 | 9 | class NamingConventionPackage /* assert: NamingConventionPackage 10 | ^ 11 | class NamingConventionPackage は fix.pixiv パッケージに実装すべきではありません 12 | */ 13 | extends AnyRef { 14 | val value: String = "string" 15 | } -------------------------------------------------------------------------------- /input/src/main/scala/fix/pixiv/NeedMessageExtendsRuntimeException.scala: -------------------------------------------------------------------------------- 1 | /* 2 | rule = NeedMessageExtendsRuntimeException 3 | */ 4 | package fix.pixiv 5 | 6 | class GreenException extends RuntimeException("message") {} 7 | 8 | class RedException extends RuntimeException {}/* assert: NeedMessageExtendsRuntimeException 9 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 10 | RuntimeException を継承したクラスを作る際にはメッセージを付与してください: RedException 11 | */ 12 | 13 | // RuntimeExceptionを継承したクラスの継承は無視しています。 14 | // ref: https://github.com/pixiv/scalafix-pixiv-rule/pull/64#discussion_r1145841349 15 | class TestClass extends RedException {} 16 | -------------------------------------------------------------------------------- /input/src/main/scala/fix/pixiv/NonCaseException.scala: -------------------------------------------------------------------------------- 1 | /* 2 | rule = NonCaseException 3 | */ 4 | package fix.pixiv 5 | 6 | case class NonCaseException(msg: String) extends RuntimeException(msg) /* assert: NonCaseException 7 | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ 8 | case class として Exception を継承することは推奨されません: NonCaseException 9 | */ 10 | -------------------------------------------------------------------------------- /input/src/main/scala/fix/pixiv/PackageJavaxToJakarta.scala: -------------------------------------------------------------------------------- 1 | /* 2 | rule = PackageJavaxToJakarta 3 | */ 4 | package fix.pixiv 5 | 6 | import javax.inject.Inject 7 | 8 | class PackageJavaxToJakarta @Inject()() { 9 | } 10 | -------------------------------------------------------------------------------- /input/src/main/scala/fix/pixiv/SingleConditionMatch.scala: -------------------------------------------------------------------------------- 1 | /* 2 | rule = SingleConditionMatch 3 | */ 4 | package fix.pixiv 5 | 6 | object SingleConditionMatch { 7 | implicit class slash(i: Int) { 8 | def \(str: String) = "" 9 | } 10 | 11 | private val result = Some(1) match { 12 | case result => result 13 | } 14 | 15 | Some(1) match { 16 | case Some(result) => println(result) 17 | } 18 | 19 | Some(1) match { 20 | case result => println(result) 21 | } 22 | 23 | Some(1) match { 24 | case result => println(result.getOrElse(result)) 25 | } 26 | 27 | Some(1) match { 28 | case result => 29 | println(result) 30 | println(result) 31 | } 32 | 33 | Some(1) match { 34 | case Some(result) => 35 | result match { 36 | case result => println(result) 37 | } 38 | } 39 | 40 | Some(1) match { 41 | case result => 42 | result match { 43 | case Some(result2) => println(result2) 44 | case _ => println("hoge") 45 | } 46 | } 47 | 48 | (123 \ "test").length match { 49 | case i => i.compareTo(2) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /input/src/main/scala/fix/pixiv/UnifiedArrow.scala: -------------------------------------------------------------------------------- 1 | /* 2 | rule = UnifiedArrow 3 | */ 4 | package fix.pixiv 5 | 6 | object UnifiedArrow { 7 | List(1 → "a", 2 → "b", 3 → "c").map { 8 | case (_, s) ⇒ s 9 | } 10 | for { 11 | a ← List(1, 2, 3) 12 | } yield a 13 | } 14 | -------------------------------------------------------------------------------- /input/src/main/scala/fix/pixiv/UnnecessarySemicolon.scala: -------------------------------------------------------------------------------- 1 | /* 2 | rule = UnnecessarySemicolon 3 | */ 4 | package fix.pixiv 5 | 6 | object UnnecessarySemicolon { 7 | val x = 3; 8 | println(x) 9 | 10 | val a = 1; val b = 2 11 | } 12 | -------------------------------------------------------------------------------- /input/src/main/scala/fix/pixiv/ZeroIndexToHead.scala: -------------------------------------------------------------------------------- 1 | /* 2 | rule = ZeroIndexToHead 3 | */ 4 | package fix.pixiv 5 | 6 | object ZeroIndexToHead { 7 | val seq = Seq(1, 2, 3) 8 | seq(0) 9 | 10 | Seq(0) // これは apply 相当のため .head に変換されない 11 | 12 | Seq(1, 2, 3)(0) 13 | 14 | val list = List(1, 2, 3) 15 | list(0) 16 | 17 | val indexed = IndexedSeq(1, 2, 3) 18 | indexed(0) 19 | 20 | Some(Seq(1, 2, 3)).get(0) // このパターンではまだ型の取得が実装できていないので変換されない 21 | } 22 | -------------------------------------------------------------------------------- /output/src/main/scala-2.13/fix/pixiv/MockitoThenToDo.scala: -------------------------------------------------------------------------------- 1 | package fix.pixiv 2 | 3 | import org.mockito.Mockito.when 4 | import org.mockito.{ArgumentMatchers, Mockito} 5 | 6 | class MockitoThenToDo { 7 | class A { 8 | def hoge(): String = "hoge" 9 | def fuga: String = "fuga" 10 | def piyo(s: String): String = s"piyo: $s" 11 | def foo(a: Int)(b: Int): String = "foo" 12 | } 13 | private val a = Mockito.mock(classOf[A]) 14 | Mockito.doReturn("mock1").doReturn("mock2").when(a).hoge() 15 | Mockito.doReturn("mock1", "mock2").when(a).fuga 16 | Mockito.doAnswer { invocation => 17 | val s = invocation.getArgument[String](0) 18 | s"mock: $s" 19 | }.when(a).piyo(ArgumentMatchers.any()) 20 | Mockito.doThrow(new RuntimeException("mock")).when(a).foo(1)(2) 21 | println(a.hoge()) 22 | println(a.fuga) 23 | println(a.piyo("")) 24 | println(a.foo(1)(2)) 25 | } 26 | -------------------------------------------------------------------------------- /output/src/main/scala-2.13/fix/pixiv/ScalatestAssertThrowsToIntercept.scala: -------------------------------------------------------------------------------- 1 | package fix.pixiv 2 | 3 | import org.scalatest.Assertions 4 | import org.scalatest.Assertions._ 5 | 6 | object ScalatestAssertThrowsToIntercept { 7 | Assertions.intercept[RuntimeException] { 8 | throw new RuntimeException("assertThrows1") 9 | } 10 | intercept[RuntimeException] { 11 | throw new RuntimeException("assertThrows2") 12 | } 13 | // 変更なし 14 | println("assertThrows") 15 | } 16 | -------------------------------------------------------------------------------- /output/src/main/scala-2.13/fix/pixiv/UnifyEmptyList.scala: -------------------------------------------------------------------------------- 1 | package fix.pixiv 2 | 3 | object UnifyEmptyList { 4 | // 変換する 5 | private val list = Nil 6 | private val nothingList = Nil 7 | private val empty = Nil 8 | private val varType: List[String] = Nil 9 | List(1, 2).foldLeft[List[Int]](Nil)((l, a) => l :+ a) 10 | varType match { 11 | case Nil => 0 12 | case "1" :: "2" :: Nil => 2 13 | case _ => -1 14 | } 15 | private def func(list: List[String] = Nil): Unit = {} 16 | // empty に変換する 17 | private val listType = List.empty[String] 18 | List(1, 2).foldLeft(List.empty[Int])((l, a) => l :+ a) 19 | private def typeFunc(list: List[String] = List.empty[String]): Unit = {} 20 | // 変換しない 21 | private val emptyType = List.empty[String] 22 | List(1, 2).foldLeft(List.empty[Int])((l, a) => l :+ a) 23 | } 24 | -------------------------------------------------------------------------------- /output/src/main/scala/fix/pixiv/CheckIsEmpty.scala: -------------------------------------------------------------------------------- 1 | package fix.pixiv 2 | 3 | object CheckIsEmpty { 4 | private val seq = Seq(1, 2, 3) 5 | private val option = Some(1) 6 | 7 | // to isEmpty 8 | Seq(1, 2, 3).isEmpty 9 | seq.isEmpty 10 | seq.isEmpty 11 | seq.isEmpty 12 | 13 | // to nonEmpty 14 | seq.nonEmpty 15 | seq.nonEmpty 16 | seq.nonEmpty 17 | seq.nonEmpty 18 | seq.nonEmpty 19 | seq.nonEmpty 20 | seq.nonEmpty 21 | seq.nonEmpty 22 | seq.nonEmpty 23 | seq.nonEmpty 24 | seq.nonEmpty 25 | 26 | // to isEmpty 27 | option.isEmpty 28 | option.isEmpty 29 | option.isEmpty 30 | option.isEmpty 31 | 32 | // to isDefined 33 | option.isDefined 34 | option.isDefined 35 | option.isDefined 36 | 37 | // 配列には isEmpty が存在しないので無視する 38 | Array(1, 2, 3).length == 0 39 | 40 | case class InSeq(seq: Seq[Int]) { 41 | def getSeq(i: Int): Seq[Int] = seq.slice(0, i) 42 | } 43 | private val inSeq = InSeq(Seq(1, 2, 3)) 44 | inSeq.seq.isEmpty 45 | inSeq.getSeq(2).isEmpty 46 | } 47 | -------------------------------------------------------------------------------- /output/src/main/scala/fix/pixiv/NeedMessageExtendsRuntimeException.scala: -------------------------------------------------------------------------------- 1 | package fix.pixiv 2 | 3 | class GreenException extends RuntimeException("message") {} 4 | 5 | class RedException extends RuntimeException {} 6 | 7 | // RuntimeExceptionを継承したクラスの継承は無視しています。 8 | // ref: https://github.com/pixiv/scalafix-pixiv-rule/pull/64#discussion_r1145841349 9 | class TestClass extends RedException {} 10 | -------------------------------------------------------------------------------- /output/src/main/scala/fix/pixiv/NonCaseException.scala: -------------------------------------------------------------------------------- 1 | package fix.pixiv 2 | 3 | case class NonCaseException(msg: String) extends RuntimeException(msg) 4 | -------------------------------------------------------------------------------- /output/src/main/scala/fix/pixiv/PackageJavaxToJakarta.scala: -------------------------------------------------------------------------------- 1 | package fix.pixiv 2 | 3 | import jakarta.inject.Inject 4 | 5 | class PackageJavaxToJakarta @Inject()() { 6 | } 7 | -------------------------------------------------------------------------------- /output/src/main/scala/fix/pixiv/SingleConditionMatch.scala: -------------------------------------------------------------------------------- 1 | package fix.pixiv 2 | 3 | object SingleConditionMatch { 4 | implicit class slash(i: Int) { 5 | def \(str: String) = "" 6 | } 7 | 8 | private val result = Some(1) 9 | 10 | Some(1).foreach { 11 | result => println(result) 12 | } 13 | 14 | println(Some(1)) 15 | 16 | 17 | { 18 | val result = Some(1) 19 | println(result.getOrElse(result)) 20 | } 21 | 22 | 23 | { 24 | val result = Some(1) 25 | println(result) 26 | println(result) 27 | } 28 | 29 | Some(1) match { 30 | case Some(result) => 31 | println(result) 32 | } 33 | 34 | Some(1) match { 35 | case Some(result2) => println(result2) 36 | case _ => println("hoge") 37 | } 38 | 39 | (123 \ "test").length.compareTo(2) 40 | } -------------------------------------------------------------------------------- /output/src/main/scala/fix/pixiv/UnifiedArrow.scala: -------------------------------------------------------------------------------- 1 | package fix.pixiv 2 | 3 | object UnifiedArrow { 4 | List(1 -> "a", 2 -> "b", 3 -> "c").map { 5 | case (_, s) => s 6 | } 7 | for { 8 | a <- List(1, 2, 3) 9 | } yield a 10 | } 11 | -------------------------------------------------------------------------------- /output/src/main/scala/fix/pixiv/UnnecessarySemicolon.scala: -------------------------------------------------------------------------------- 1 | package fix.pixiv 2 | 3 | object UnnecessarySemicolon { 4 | val x = 3 5 | println(x) 6 | 7 | val a = 1; val b = 2 8 | } 9 | -------------------------------------------------------------------------------- /output/src/main/scala/fix/pixiv/ZeroIndexToHead.scala: -------------------------------------------------------------------------------- 1 | package fix.pixiv 2 | 3 | object ZeroIndexToHead { 4 | val seq = Seq(1, 2, 3) 5 | seq.head 6 | 7 | Seq(0) // これは apply 相当のため .head に変換されない 8 | 9 | Seq(1, 2, 3).head 10 | 11 | val list = List(1, 2, 3) 12 | list.head 13 | 14 | val indexed = IndexedSeq(1, 2, 3) 15 | indexed(0) 16 | 17 | Some(Seq(1, 2, 3)).get(0) // このパターンではまだ型の取得が実装できていないので変換されない 18 | } 19 | -------------------------------------------------------------------------------- /project/TargetAxis.scala: -------------------------------------------------------------------------------- 1 | import sbt._ 2 | import sbt.internal.ProjectMatrix 3 | import sbtprojectmatrix.ProjectMatrixPlugin.autoImport._ 4 | 5 | /** Use on ProjectMatrix rows to tag an affinity to a custom scalaVersion */ 6 | case class TargetAxis(scalaVersion: String) extends VirtualAxis.WeakAxis { 7 | 8 | private val scalaBinaryVersion = CrossVersion.binaryScalaVersion(scalaVersion) 9 | 10 | override val idSuffix = s"Target${scalaBinaryVersion.replace('.', '_')}" 11 | override val directorySuffix = s"target$scalaBinaryVersion" 12 | } 13 | 14 | object TargetAxis { 15 | 16 | private def targetScalaVersion(virtualAxes: Seq[VirtualAxis]): String = 17 | virtualAxes.collectFirst { case a: TargetAxis => a.scalaVersion }.get 18 | 19 | /** 20 | * When invoked on a ProjectMatrix with a TargetAxis, lookup the project 21 | * generated by `matrix` with a scalaVersion matching the one declared in 22 | * that TargetAxis, and resolve `key`. 23 | */ 24 | def resolve[T]( 25 | matrix: ProjectMatrix, 26 | key: TaskKey[T] 27 | ): Def.Initialize[Task[T]] = 28 | Def.taskDyn { 29 | val sv = targetScalaVersion(virtualAxes.value) 30 | val project = matrix.finder().apply(sv) 31 | Def.task((project / key).value) 32 | } 33 | 34 | /** 35 | * When invoked on a ProjectMatrix with a TargetAxis, lookup the project 36 | * generated by `matrix` with a scalaVersion matching the one declared in 37 | * that TargetAxis, and resolve `key`. 38 | */ 39 | def resolve[T]( 40 | matrix: ProjectMatrix, 41 | key: SettingKey[T] 42 | ): Def.Initialize[T] = 43 | Def.settingDyn { 44 | val sv = targetScalaVersion(virtualAxes.value) 45 | val project = matrix.finder().apply(sv) 46 | Def.setting((project / key).value) 47 | } 48 | 49 | } 50 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.9.2 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | resolvers += Resolver.sonatypeRepo("releases") 2 | addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.11.0") 3 | addSbtPlugin("com.eed3si9n" % "sbt-projectmatrix" % "0.9.1") 4 | addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.0") 5 | addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.12") 6 | addSbtPlugin("com.github.sbt" % "sbt-license-report" % "1.5.0") 7 | -------------------------------------------------------------------------------- /rules/src/main/resources/META-INF/services/scalafix.v1.Rule: -------------------------------------------------------------------------------- 1 | fix.pixiv.UnnecessarySemicolon 2 | fix.pixiv.ZeroIndexToHead 3 | fix.pixiv.CheckIsEmpty 4 | fix.pixiv.NonCaseException 5 | fix.pixiv.UnifyEmptyList 6 | fix.pixiv.SingleConditionMatch 7 | fix.pixiv.MockitoThenToDo 8 | fix.pixiv.UnifiedArrow 9 | fix.pixiv.ScalatestAssertThrowsToIntercept 10 | fix.pixiv.NeedMessageExtendsRuntimeException 11 | fix.pixiv.NamingConventionPackage 12 | fix.pixiv.PackageJavaxToJakarta -------------------------------------------------------------------------------- /rules/src/main/scala/fix/pixiv/CheckIsEmpty.scala: -------------------------------------------------------------------------------- 1 | package fix.pixiv 2 | 3 | import scala.collection.immutable.{NumericRange, Queue} 4 | import scala.collection.mutable 5 | import scala.meta.{Lit, Term, Tree} 6 | 7 | import fix.pixiv.CheckIsEmpty.isType 8 | import metaconfig.Configured 9 | import scalafix.v1.{Configuration, Patch, Rule, SemanticDocument, SemanticRule, XtensionTreeScalafix} 10 | import util.SymbolConverter.SymbolToSemanticType 11 | 12 | class CheckIsEmpty(config: CheckIsEmptyConfig) extends SemanticRule("CheckIsEmpty") { 13 | def this() = this(CheckIsEmptyConfig.default) 14 | override def withConfiguration(config: Configuration): Configured[Rule] = { 15 | config.conf.getOrElse("CheckIsEmpty")(this.config) 16 | .map { newConfig => new CheckIsEmpty(newConfig) } 17 | } 18 | 19 | override def fix(implicit doc: SemanticDocument): Patch = { 20 | implicit val alignIsDefined: Boolean = config.alignIsDefined 21 | doc.tree.collect { 22 | case t @ IsDefined(x1, rewrite) if rewrite && isType(x1, classOf[Option[Any]]) => 23 | Patch.replaceTree(t, Term.Select(x1, Term.Name("isDefined")).toString()) 24 | case t @ NonEmpty(x1, rewrite) if rewrite && CheckIsEmpty.isTypeHasIsEmpty(x1) => 25 | Patch.replaceTree(t, Term.Select(x1, Term.Name("nonEmpty")).toString()) 26 | case t @ IsEmpty(x1, rewrite) if rewrite && CheckIsEmpty.isTypeHasIsEmpty(x1) => 27 | Patch.replaceTree(t, Term.Select(x1, Term.Name("isEmpty")).toString()) 28 | }.asPatch 29 | } 30 | } 31 | 32 | private object CheckIsEmpty { 33 | def isTypeHasIsEmpty(x1: Term)(implicit doc: SemanticDocument): Boolean = { 34 | Seq( 35 | classOf[Seq[Any]], 36 | Seq.getClass, 37 | Vector.getClass, 38 | NumericRange.getClass, 39 | // String はこのルールでは扱わない 40 | Range.getClass, 41 | List.getClass, 42 | Queue.getClass, 43 | classOf[mutable.Seq[Any]], 44 | classOf[Option[Any]], 45 | Option.getClass, 46 | Some.getClass, 47 | None.getClass 48 | ).exists { clazz => 49 | isType(x1, clazz) 50 | } 51 | } 52 | 53 | def isType(x1: Term, clazz: Class[_])(implicit doc: SemanticDocument): Boolean = { 54 | try { 55 | x1 match { 56 | case x1: Term.Name => x1.symbol.isAssignableTo(clazz) 57 | case x1 @ Term.Apply(_: Term.Name, _) => x1.symbol.isAssignableTo(clazz) 58 | case _ @Term.Select(_, x1: Term.Name) => x1.symbol.isAssignableTo(clazz) 59 | case _ @Term.Apply(_ @Term.Select(_, x1: Term.Name), _) => x1.symbol.isAssignableTo(clazz) 60 | case _ => false 61 | } 62 | } catch { 63 | case _: Throwable => false 64 | } 65 | } 66 | } 67 | 68 | private object IsEmpty { 69 | def unapply(tree: Tree)(implicit doc: SemanticDocument): Option[(Term, Boolean)] = { 70 | implicit val alignIsDefined: Boolean = true 71 | tree match { 72 | // `seq.isEmpty` は変換不要だが IsEmpty ではある 73 | case _ @Term.Select(x1: Term, _ @Term.Name("isEmpty")) => Some(x1, false) 74 | // `seq.size == 0` 75 | case _ @Term.ApplyInfix( 76 | Term.Select(x1: Term, _ @(Term.Name("size") | Term.Name("length"))), 77 | Term.Name("=="), 78 | Nil, 79 | List(Lit.Int(0)) 80 | ) => Some((x1, true)) 81 | case _ @Term.ApplyUnary(Term.Name("!"), NonEmpty(x1, _)) => Some((x1, true)) 82 | case _ @Term.ApplyUnary(Term.Name("!"), IsDefined(x1, _)) => Some((x1, true)) 83 | // option == None 84 | case _ @Term.ApplyInfix(x1: Term, _ @Term.Name("=="), Nil, List(Term.Name("None"))) 85 | if isType(x1, classOf[Option[Any]]) => Some((x1, true)) 86 | // None == option 87 | case _ @Term.ApplyInfix(Term.Name("None"), _ @Term.Name("=="), Nil, List(x1: Term)) 88 | if isType(x1, classOf[Option[Any]]) => Some((x1, true)) 89 | case _ => None 90 | } 91 | } 92 | } 93 | 94 | private object NonEmpty { 95 | def unapply(tree: Tree)(implicit doc: SemanticDocument): Option[(Term, Boolean)] = tree match { 96 | // `seq.nonEmpty` は変換不要だが NonEmpty ではある 97 | case _ @Term.Select(x1: Term, _ @Term.Name("nonEmpty")) => Some(x1, false) 98 | // `seq.size != 0` or `seq.size > 0` 99 | case _ @Term.ApplyInfix( 100 | Term.Select(x1: Term, _ @(Term.Name("size") | Term.Name("length"))), 101 | _ @(Term.Name("!=") | Term.Name(">")), 102 | Nil, 103 | List(Lit.Int(0)) 104 | ) => Some((x1, true)) 105 | // `seq.size > 1` 106 | case _ @Term.ApplyInfix( 107 | Term.Select(x1: Term, _ @(Term.Name("size") | Term.Name("length"))), 108 | _ @Term.Name(">="), 109 | Nil, 110 | List(Lit.Int(1)) 111 | ) => Some((x1, true)) 112 | // `seq.exists(_ => true)` 113 | case _ @Term.Apply( 114 | Term.Select(x1, _ @Term.Name("exists")), 115 | List(Term.Function((_, Lit.Boolean(true)))) 116 | ) => Some((x1, true)) 117 | // `seq.exists(Function.const(true))` 118 | case _ @Term.Apply( 119 | Term.Select(x1, _ @Term.Name("exists")), 120 | List(Term.Apply(Term.Select(Term.Name("Function"), Term.Name("const")), List(Lit.Boolean(true)))) 121 | ) => Some((x1, true)) 122 | case _ @Term.ApplyUnary(Term.Name("!"), IsEmpty(x1, _)) if !isType(x1, classOf[Option[Any]]) => 123 | Some((x1, true)) 124 | case _ => None 125 | } 126 | } 127 | 128 | private object IsDefined { 129 | def unapply(tree: Tree)(implicit doc: SemanticDocument, alignIsDefined: Boolean): Option[(Term, Boolean)] = 130 | tree match { 131 | // `option.isDefined` は変換不要だが IsDefined ではある 132 | case _ @Term.Select(x1: Term, _ @Term.Name("isDefined")) => Some(x1, false) 133 | // option != None 134 | case _ @Term.ApplyInfix(x1: Term, _ @Term.Name("!="), Nil, List(Term.Name("None"))) => Some((x1, true)) 135 | // None != option 136 | case _ @Term.ApplyInfix(Term.Name("None"), _ @Term.Name("!="), Nil, List(x1: Term)) => Some((x1, true)) 137 | case _ @NonEmpty(Term.Placeholder(), _) => None 138 | case _ @Term.ApplyUnary(Term.Name("!"), IsEmpty(x1, _)) if isType(x1, classOf[Option[Any]]) => 139 | Some((x1, true)) 140 | // option.nonEmpty => option.isDefined 141 | case _ @NonEmpty(x1, _) if isType(x1, classOf[Option[Any]]) => Some((x1, alignIsDefined)) 142 | case _ => None 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /rules/src/main/scala/fix/pixiv/CheckIsEmptyConfig.scala: -------------------------------------------------------------------------------- 1 | package fix.pixiv 2 | 3 | import metaconfig.ConfDecoder 4 | import metaconfig.generic.Surface 5 | 6 | final case class CheckIsEmptyConfig(alignIsDefined: Boolean = false) 7 | 8 | object CheckIsEmptyConfig { 9 | val default: CheckIsEmptyConfig = CheckIsEmptyConfig() 10 | implicit val surface: Surface[CheckIsEmptyConfig] = metaconfig.generic.deriveSurface[CheckIsEmptyConfig] 11 | implicit val decoder: ConfDecoder[CheckIsEmptyConfig] = metaconfig.generic.deriveDecoder(default) 12 | } 13 | -------------------------------------------------------------------------------- /rules/src/main/scala/fix/pixiv/LogConfig.scala: -------------------------------------------------------------------------------- 1 | package fix.pixiv 2 | 3 | import metaconfig.ConfDecoder 4 | import metaconfig.generic.Surface 5 | 6 | final case class LogConfig( 7 | level: String = "Warn" 8 | ) 9 | 10 | object LogConfig { 11 | val default: LogConfig = LogConfig() 12 | implicit val surface: Surface[LogConfig] = metaconfig.generic.deriveSurface[LogConfig] 13 | implicit val decoder: ConfDecoder[LogConfig] = metaconfig.generic.deriveDecoder(default) 14 | } 15 | -------------------------------------------------------------------------------- /rules/src/main/scala/fix/pixiv/LogLevel.scala: -------------------------------------------------------------------------------- 1 | package fix.pixiv 2 | 3 | import scala.meta.Position 4 | 5 | import scalafix.lint.{Diagnostic, LintSeverity} 6 | 7 | case class LogLevel(override val message: String, override val position: Position, level: String) extends Diagnostic { 8 | override def severity: LintSeverity = level match { 9 | case "Info" => LintSeverity.Info 10 | case "Warn" => LintSeverity.Warning 11 | case "Error" => LintSeverity.Error 12 | // 設定に異常値が入っている場合はデフォルトとして扱う 13 | case _ => LintSeverity.Warning 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /rules/src/main/scala/fix/pixiv/MockitoThenToDo.scala: -------------------------------------------------------------------------------- 1 | package fix.pixiv 2 | 3 | import scala.meta.{Term, Tree} 4 | 5 | import scalafix.Patch 6 | import scalafix.v1.{SemanticDocument, SemanticRule} 7 | import util.TreeUtil.ShallowCollectTree 8 | 9 | class MockitoThenToDo extends SemanticRule("MockitoThenToDo") { 10 | override def fix(implicit doc: SemanticDocument): Patch = { 11 | doc.tree.shallowCollect { 12 | case t @ MockitoThen(target, func, args, thenTerm) => 13 | Some(Patch.replaceTree(t, MockitoDo(target, func, args, thenTerm).syntax)) 14 | case _ => None 15 | }.asPatch 16 | } 17 | } 18 | 19 | private object MockitoDo { 20 | private val mathodNameConverter = (s: String) => 21 | s match { 22 | case "thenReturn" => "doReturn" 23 | case "thenAnswer" => "doAnswer" 24 | case "thenThrow" => "doThrow" 25 | case method => throw new RuntimeException(s"Mockito の then メソッドでない可能性があります: $method") 26 | } 27 | 28 | def apply(target: Term.Name, func: Term.Name, args: List[List[Term]], thenTerm: List[(String, List[Term])]): Term = { 29 | val doTherm = thenTerm.foldLeft[Term] { 30 | Term.Name("Mockito") 31 | } { case (term, (method, result)) => 32 | Term.Apply( 33 | Term.Select( 34 | term, 35 | Term.Name(mathodNameConverter(method)) 36 | ), 37 | result 38 | ) 39 | } 40 | args match { 41 | case Nil => 42 | Term.Select( 43 | Term.Apply( 44 | Term.Select( 45 | doTherm, 46 | Term.Name("when") 47 | ), 48 | List(func) 49 | ), 50 | target 51 | ) 52 | case list: List[List[Term]] => 53 | list.foldLeft[Term]( 54 | Term.Select( 55 | Term.Apply( 56 | Term.Select( 57 | doTherm, 58 | Term.Name("when") 59 | ), 60 | List(func) 61 | ), 62 | target 63 | ) 64 | ) { 65 | case (select, arg) => Term.Apply(select, arg) 66 | } 67 | } 68 | } 69 | } 70 | 71 | private object MockitoThen { 72 | val methodNames: List[String] = 73 | List("thenReturn", "thenAnswer", "thenThrow") 74 | def unapply(tree: Tree): Option[(Term.Name, Term.Name, List[List[Term]], List[(String, List[Term])])] = tree match { 75 | case Term.Apply( 76 | Term.Select( 77 | MockitoThen(target, func, args, thenTerm: List[(String, List[Term])]), 78 | Term.Name(method) 79 | ), 80 | result 81 | ) if methodNames.contains(method) => 82 | Some(target, func, args, thenTerm :+ (method, result)) 83 | case Term.Apply( 84 | (Term.Select( 85 | Term.Name("Mockito"), 86 | Term.Name("when") 87 | ) | Term.Name("when")), 88 | List(MockitoThenInternal(target, func, args)) 89 | ) => Some(target, func, args, Nil) 90 | case _ => None 91 | } 92 | 93 | private object MockitoThenInternal { 94 | def unapply(tree: Tree): Option[(Term.Name, Term.Name, List[List[Term]])] = tree match { 95 | case Term.Apply( 96 | MockitoThenInternal(target, func, args), 97 | arg 98 | ) => Some(target, func, args :+ arg) 99 | case Term.Select(func: Term.Name, target) => Some(target, func, Nil) 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /rules/src/main/scala/fix/pixiv/NamingConventionPackage.scala: -------------------------------------------------------------------------------- 1 | package fix.pixiv 2 | 3 | import scala.meta.{Defn, Pkg} 4 | import scala.util.matching.Regex 5 | 6 | import metaconfig.Configured 7 | import scalafix.Patch 8 | import scalafix.v1.{Configuration, Rule, SemanticDocument, SemanticRule} 9 | 10 | class NamingConventionPackage(config: LogConfig, namingConventionPackageConfig: NamingConventionPackageConfig) 11 | extends SemanticRule("NamingConventionPackage") { 12 | def this() = this(LogConfig(), NamingConventionPackageConfig()) 13 | 14 | override def withConfiguration(config: Configuration): Configured[Rule] = { 15 | (config.conf.getOrElse("NamingConventionPackage")(this.config) |@| config.conf.getOrElse("NamingConventionPackage")( 16 | this.namingConventionPackageConfig 17 | )) 18 | .map { 19 | case (config, config1) => new NamingConventionPackage(config, config1) 20 | } 21 | } 22 | 23 | private val patterns: List[(Regex, Regex)] = namingConventionPackageConfig.convention.map { map => 24 | val (pkg, cls) = (for { 25 | pkg <- map.get("package") 26 | cls <- map.get("class") 27 | } yield (pkg, cls)) 28 | .getOrElse(throw new RuntimeException(s"Config の指定が不正です: ${map.mkString(", ")}")) 29 | pkg.r -> cls.r 30 | } 31 | 32 | override def fix(implicit doc: SemanticDocument): Patch = { 33 | doc.tree.collect { 34 | case Pkg(packageName, list) => 35 | list.map { 36 | case t @ Defn.Class(_, className, _, _, _) => 37 | patterns.find { 38 | case (pkg, cls) => 39 | pkg.findFirstIn(packageName.toString()).nonEmpty && cls.findFirstIn(className.value).nonEmpty 40 | }.fold(Patch.empty) { _: (Regex, Regex) => 41 | Patch.lint(LogLevel( 42 | s"class ${className.value} は ${packageName.toString()} パッケージに実装すべきではありません", 43 | t.pos, 44 | config.level 45 | )) 46 | } 47 | case _ => Patch.empty 48 | }.asPatch 49 | }.asPatch 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /rules/src/main/scala/fix/pixiv/NamingConventionPackageConfig.scala: -------------------------------------------------------------------------------- 1 | package fix.pixiv 2 | import metaconfig.ConfDecoder 3 | import metaconfig.generic.Surface 4 | 5 | case class NamingConventionPackageConfig( 6 | convention: List[Map[String, String]] = List.empty 7 | ) 8 | 9 | object NamingConventionPackageConfig { 10 | val default: NamingConventionPackageConfig = NamingConventionPackageConfig() 11 | implicit val surface: Surface[NamingConventionPackageConfig] = 12 | metaconfig.generic.deriveSurface[NamingConventionPackageConfig] 13 | implicit val decoder: ConfDecoder[NamingConventionPackageConfig] = metaconfig.generic.deriveDecoder(default) 14 | } 15 | -------------------------------------------------------------------------------- /rules/src/main/scala/fix/pixiv/NeedMessageExtendsRuntimeException.scala: -------------------------------------------------------------------------------- 1 | package fix.pixiv 2 | 3 | import scala.meta.{Defn, Init, Position, Template} 4 | 5 | import metaconfig.Configured 6 | import scalafix.Patch 7 | import scalafix.lint.{Diagnostic, LintSeverity} 8 | import scalafix.v1.{Configuration, Rule, SemanticDocument, SemanticRule, XtensionTreeScalafix} 9 | import util.SemanticTypeConverter.SemanticTypeToClass 10 | import util.SymbolConverter.SymbolToSemanticType 11 | 12 | class NeedMessageExtendsRuntimeException(config: LogConfig) extends SemanticRule("NeedMessageExtendsRuntimeException") { 13 | def this() = this(LogConfig()) 14 | 15 | override def withConfiguration(config: Configuration): Configured[Rule] = { 16 | config.conf.getOrElse("NeedMessageExtendsRuntimeException")(this.config) 17 | .map { newConfig => new NeedMessageExtendsRuntimeException(newConfig) } 18 | } 19 | 20 | override def fix(implicit doc: SemanticDocument): Patch = { 21 | doc.tree.collect { 22 | case t @ Defn.Class( 23 | _, 24 | name, 25 | _, 26 | _, 27 | _ @Template(_, List(_ @Init(typ, _, msg)), _, _) 28 | ) => 29 | try { 30 | if (typ.symbol.toSemanticType.toClass == classOf[RuntimeException] && msg.flatten.isEmpty) { 31 | Patch.lint(LogLevel( 32 | s"RuntimeException を継承したクラスを作る際にはメッセージを付与してください: $name", 33 | t.pos, 34 | config.level 35 | )) 36 | } else { 37 | Patch.empty 38 | } 39 | } catch { 40 | case _: Throwable => Patch.empty 41 | } 42 | }.asPatch 43 | } 44 | } 45 | 46 | case class NeedMessageExtendsRuntimeExceptionError(override val message: String, position: Position) 47 | extends Diagnostic { 48 | override def severity: LintSeverity = LintSeverity.Error 49 | } 50 | -------------------------------------------------------------------------------- /rules/src/main/scala/fix/pixiv/NonCaseException.scala: -------------------------------------------------------------------------------- 1 | package fix.pixiv 2 | 3 | import scala.meta.{Defn, Init, Mod, Template} 4 | 5 | import metaconfig.Configured 6 | import scalafix.Patch 7 | import scalafix.v1.{Configuration, Rule, SemanticDocument, SemanticRule, XtensionTreeScalafix} 8 | import util.SymbolConverter.SymbolToSemanticType 9 | 10 | class NonCaseException(config: LogConfig) extends SemanticRule("NonCaseException") { 11 | def this() = this(LogConfig()) 12 | 13 | override def withConfiguration(config: Configuration): Configured[Rule] = { 14 | config.conf.getOrElse("NonCaseException")(this.config) 15 | .map { newConfig => new NonCaseException(newConfig) } 16 | } 17 | 18 | override def fix(implicit doc: SemanticDocument): Patch = { 19 | doc.tree.collect { 20 | case t @ Defn.Class(List(_: Mod.Case), name, _, _, _ @Template(_, List(_ @Init(typ, _, _)), _, _)) => 21 | try { 22 | if (typ.symbol.isAssignableTo(classOf[Exception])) { 23 | Patch.lint(LogLevel(s"case class として Exception を継承することは推奨されません: $name", t.pos, config.level)) 24 | } else { 25 | Patch.empty 26 | } 27 | } catch { 28 | case _: Throwable => Patch.empty 29 | } 30 | }.asPatch 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /rules/src/main/scala/fix/pixiv/PackageJavaxToJakarta.scala: -------------------------------------------------------------------------------- 1 | package fix.pixiv 2 | 3 | import scala.meta.{Importer, Term} 4 | 5 | import scalafix.Patch 6 | import scalafix.v1.{SemanticDocument, SemanticRule} 7 | 8 | class PackageJavaxToJakarta extends SemanticRule("PackageJavaxToJakarta") { 9 | 10 | private final val mappings = Map( 11 | "javax.activation" -> "jakarta.activation", 12 | "javax.annotation" -> "jakarta.annotation", 13 | "javax.annotation.security" -> "jakarta.annotation.security", 14 | "javax.annotation.sql" -> "jakarta.annotation.sql", 15 | "javax.batch" -> "jakarta.batch", 16 | "javax.batch.api" -> "jakarta.batch.api", 17 | "javax.batch.api.chunk" -> "jakarta.batch.api.chunk", 18 | "javax.batch.api.chunk.listener" -> "jakarta.batch.api.chunk.listener", 19 | "javax.batch.api.listener" -> "jakarta.batch.api.listener", 20 | "javax.batch.api.partition" -> "jakarta.batch.api.partition", 21 | "javax.batch.operations" -> "jakarta.batch.operations", 22 | "javax.batch.runtime" -> "jakarta.batch.runtime", 23 | "javax.batch.runtime.context" -> "jakarta.batch.runtime.context", 24 | "javax.cache" -> "jakarta.cache", 25 | "javax.cache.annotation" -> "jakarta.cache.annotation", 26 | "javax.cache.configuration" -> "jakarta.cache.configuration", 27 | "javax.cache.event" -> "jakarta.cache.event", 28 | "javax.cache.expiry" -> "jakarta.cache.expiry", 29 | "javax.cache.integration" -> "jakarta.cache.integration", 30 | "javax.cache.management" -> "jakarta.cache.management", 31 | "javax.cache.processor" -> "jakarta.cache.processor", 32 | "javax.cache.spi" -> "jakarta.cache.spi", 33 | "javax.decorator" -> "jakarta.decorator", 34 | "javax.ejb" -> "jakarta.ejb", 35 | "javax.ejb.embeddable" -> "jakarta.ejb.embeddable", 36 | "javax.ejb.spi" -> "jakarta.ejb.spi", 37 | "javax.el" -> "jakarta.el", 38 | "javax.enterprise" -> "jakarta.enterprise", 39 | "javax.enterprise.concurrent" -> "jakarta.enterprise.concurrent", 40 | "javax.enterprise.context" -> "jakarta.enterprise.context", 41 | "javax.enterprise.context.control" -> "jakarta.enterprise.context.control", 42 | "javax.enterprise.context.spi" -> "jakarta.enterprise.context.spi", 43 | "javax.enterprise.deploy" -> "jakarta.enterprise.deploy", 44 | "javax.enterprise.deploy.model" -> "jakarta.enterprise.deploy.model", 45 | "javax.enterprise.deploy.model.exceptions" -> "jakarta.enterprise.deploy.model.exceptions", 46 | "javax.enterprise.deploy.shared" -> "jakarta.enterprise.deploy.shared", 47 | "javax.enterprise.deploy.shared.factories" -> "jakarta.enterprise.deploy.shared.factories", 48 | "javax.enterprise.deploy.spi" -> "jakarta.enterprise.deploy.spi", 49 | "javax.enterprise.deploy.spi.exceptions" -> "jakarta.enterprise.deploy.spi.exceptions", 50 | "javax.enterprise.deploy.spi.factories" -> "jakarta.enterprise.deploy.spi.factories", 51 | "javax.enterprise.deploy.spi.status" -> "jakarta.enterprise.deploy.spi.status", 52 | "javax.enterprise.event" -> "jakarta.enterprise.event", 53 | "javax.enterprise.inject" -> "jakarta.enterprise.inject", 54 | "javax.enterprise.inject.literal" -> "jakarta.enterprise.inject.literal", 55 | "javax.enterprise.inject.se" -> "jakarta.enterprise.inject.se", 56 | "javax.enterprise.inject.spi" -> "jakarta.enterprise.inject.spi", 57 | "javax.enterprise.inject.spi.configurator" -> "jakarta.enterprise.inject.spi.configurator", 58 | "javax.enterprise.util" -> "jakarta.enterprise.util", 59 | "javax.inject" -> "jakarta.inject", 60 | "javax.interceptor" -> "jakarta.interceptor", 61 | "javax.jms" -> "jakarta.jms", 62 | "javax.json" -> "jakarta.json", 63 | "javax.json.bind" -> "jakarta.json.bind", 64 | "javax.json.bind.adapter" -> "jakarta.json.bind.adapter", 65 | "javax.json.bind.annotation" -> "jakarta.json.bind.annotation", 66 | "javax.json.bind.config" -> "jakarta.json.bind.config", 67 | "javax.json.bind.serializer" -> "jakarta.json.bind.serializer", 68 | "javax.json.bind.spi" -> "jakarta.json.bind.spi", 69 | "javax.json.spi" -> "jakarta.json.spi", 70 | "javax.json.stream" -> "jakarta.json.stream", 71 | "javax.jws" -> "jakarta.jws", 72 | "javax.jws.soap" -> "jakarta.jws.soap", 73 | "javax.management" -> "jakarta.management", 74 | "javax.management.j2ee" -> "jakarta.management.j2ee", 75 | "javax.management.j2ee.statistics" -> "jakarta.management.j2ee.statistics", 76 | "javax.persistence" -> "jakarta.persistence", 77 | "javax.persistence.criteria" -> "jakarta.persistence.criteria", 78 | "javax.persistence.metamodel" -> "jakarta.persistence.metamodel", 79 | "javax.persistence.spi" -> "jakarta.persistence.spi", 80 | "javax.resource" -> "jakarta.resource", 81 | "javax.resource.cci" -> "jakarta.resource.cci", 82 | "javax.resource.spi" -> "jakarta.resource.spi", 83 | "javax.resource.spi.endpoint" -> "jakarta.resource.spi.endpoint", 84 | "javax.resource.spi.security" -> "jakarta.resource.spi.security", 85 | "javax.resource.spi.work" -> "jakarta.resource.spi.work", 86 | "javax.security.auth.message" -> "jakarta.security.auth.message", 87 | "javax.security.auth.message.callback" -> "jakarta.security.auth.message.callback", 88 | "javax.security.auth.message.config" -> "jakarta.security.auth.message.config", 89 | "javax.security.auth.message.module" -> "jakarta.security.auth.message.module", 90 | "javax.security.jacc" -> "jakarta.security.jacc", 91 | "javax.servlet" -> "jakarta.servlet", 92 | "javax.servlet.annotation" -> "jakarta.servlet.annotation", 93 | "javax.servlet.descriptor" -> "jakarta.servlet.descriptor", 94 | "javax.servlet.http" -> "jakarta.servlet.http", 95 | "javax.servlet.jsp" -> "jakarta.servlet.jsp", 96 | "javax.servlet.jsp.el" -> "jakarta.servlet.jsp.el", 97 | "javax.servlet.jsp.resources" -> "jakarta.servlet.jsp.resources", 98 | "javax.servlet.jsp.tagext" -> "jakarta.servlet.jsp.tagext", 99 | "javax.servlet.resources" -> "jakarta.servlet.resources", 100 | "javax.validation" -> "jakarta.validation", 101 | "javax.validation.bootstrap" -> "jakarta.validation.bootstrap", 102 | "javax.validation.constraints" -> "jakarta.validation.constraints", 103 | "javax.validation.constraintvalidation" -> "jakarta.validation.constraintvalidation", 104 | "javax.validation.executable" -> "jakarta.validation.executable", 105 | "javax.validation.groups" -> "jakarta.validation.groups", 106 | "javax.validation.metadata" -> "jakarta.validation.metadata", 107 | "javax.validation.spi" -> "jakarta.validation.spi", 108 | "javax.validation.valueextraction" -> "jakarta.validation.valueextraction", 109 | "javax.websocket" -> "jakarta.websocket", 110 | "javax.websocket.server" -> "jakarta.websocket.server", 111 | "javax.ws" -> "jakarta.ws", 112 | "javax.ws.rs" -> "jakarta.ws.rs", 113 | "javax.ws.rs.client" -> "jakarta.ws.rs.client", 114 | "javax.ws.rs.container" -> "jakarta.ws.rs.container", 115 | "javax.ws.rs.core" -> "jakarta.ws.rs.core", 116 | "javax.ws.rs.ext" -> "jakarta.ws.rs.ext", 117 | "javax.ws.rs.sse" -> "jakarta.ws.rs.sse", 118 | "javax.xml" -> "jakarta.xml", 119 | "javax.xml.namespace" -> "jakarta.xml.namespace", 120 | "javax.xml.registry" -> "jakarta.xml.registry", 121 | "javax.xml.registry.infomodel" -> "jakarta.xml.registry.infomodel", 122 | "javax.xml.rpc" -> "jakarta.xml.rpc", 123 | "javax.xml.rpc.encoding" -> "jakarta.xml.rpc.encoding", 124 | "javax.xml.rpc.handler" -> "jakarta.xml.rpc.handler", 125 | "javax.xml.rpc.handler.soap" -> "jakarta.xml.rpc.handler.soap", 126 | "javax.xml.rpc.holders" -> "jakarta.xml.rpc.holders", 127 | "javax.xml.rpc.server" -> "jakarta.xml.rpc.server", 128 | "javax.xml.rpc.soap" -> "jakarta.xml.rpc.soap", 129 | "javax.xml.soap" -> "jakarta.xml.soap", 130 | "javax.xml.stream" -> "jakarta.xml.stream", 131 | "javax.xml.stream.events" -> "jakarta.xml.stream.events", 132 | "javax.xml.stream.util" -> "jakarta.xml.stream.util", 133 | "javax.xml.ws" -> "jakarta.xml.ws", 134 | "javax.xml.ws.handler" -> "jakarta.xml.ws.handler", 135 | "javax.xml.ws.handler.soap" -> "jakarta.xml.ws.handler.soap", 136 | "javax.xml.ws.http" -> "jakarta.xml.ws.http", 137 | "javax.xml.ws.soap" -> "jakarta.xml.ws.soap", 138 | "javax.xml.ws.spi" -> "jakarta.xml.ws.spi", 139 | "javax.xml.ws.spi.http" -> "jakarta.xml.ws.spi.http", 140 | "javax.xml.ws.wsaddressing" -> "jakarta.xml.ws.wsaddressing" 141 | ) 142 | 143 | override def fix(implicit doc: SemanticDocument): Patch = { 144 | doc.tree.collect { 145 | case t @ Importer(packageName @ Term.Select(Term.Name("javax"), _), name) => 146 | mappings.get(packageName.toString()) match { 147 | case Some(newPackageName) => 148 | val splitPackage = newPackageName.split("\\.").toList 149 | Patch.replaceTree( 150 | t, 151 | Importer( 152 | // String#split は必ず空ではない 153 | splitPackage.tail.foldLeft[Term.Ref](Term.Name(splitPackage.head)) { (term, name) => 154 | Term.Select(term, Term.Name(name)) 155 | }, 156 | name 157 | ).toString() 158 | ) 159 | case _ => Patch.empty 160 | } 161 | }.asPatch 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /rules/src/main/scala/fix/pixiv/ScalatestAssertThrowsToIntercept.scala: -------------------------------------------------------------------------------- 1 | package fix.pixiv 2 | 3 | import scala.meta._ 4 | 5 | import org.scalatest.compatible.Assertion 6 | import scalafix.Patch 7 | import scalafix.v1._ 8 | import util.SymbolConverter.SymbolToSemanticType 9 | class ScalatestAssertThrowsToIntercept extends SemanticRule("ScalatestAssertThrowsToIntercept") { 10 | override def fix(implicit doc: SemanticDocument): Patch = { 11 | doc.tree.collect { 12 | case Term.ApplyType(name @ Term.Name("assertThrows"), List(_)) if isAssertThrows(name) => 13 | Patch.replaceTree(name, "intercept") 14 | case Term.Select(Term.Name("Assertions"), name @ Term.Name("assertThrows")) if isAssertThrows(name) => 15 | Patch.replaceTree(name, "intercept") 16 | case _ => Patch.empty 17 | }.asPatch 18 | } 19 | private def isAssertThrows(x1: Term)(implicit doc: SemanticDocument): Boolean = { 20 | x1.symbol.isAssignableFrom(classOf[Assertion]) 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /rules/src/main/scala/fix/pixiv/SingleConditionMatch.scala: -------------------------------------------------------------------------------- 1 | package fix.pixiv 2 | 3 | import scala.meta.Term.Match 4 | import scala.meta.{Case, Defn, Pat, Term, Tree} 5 | 6 | import scalafix.Patch 7 | import scalafix.v1.{SemanticDocument, SemanticRule} 8 | 9 | class SingleConditionMatch extends SemanticRule("SingleConditionMatch") { 10 | 11 | override def fix(implicit doc: SemanticDocument): Patch = doc.tree.collect { 12 | // case a => a 13 | // SingleReturn の場合は Match が含まれることはない 14 | case SimpleReturn(t) => Patch.replaceTree(t, t.expr.toString) 15 | // case a => {f(a); g(a)} 16 | case HasBlock(t, pName, expr, b) if !hasMatch(b) => 17 | toBlockPatch(t, pName.value, expr, b) 18 | // case a => f(a) 19 | case HasSingleState(t, pName, expr, b) if !hasMatch(b) => 20 | toSingleReplaceIfOnceUseVal(t, pName.value, expr, b) 21 | // case Some(a) => f(a) 22 | case UnapplySome(t, pName, expr, b) if !hasMatch(b) => 23 | Patch.replaceTree( 24 | t, 25 | Term.Apply( 26 | Term.Select(expr, Term.Name("foreach")), 27 | List(Term.Block(List(Term.Function(List(Term.Param(Nil, pName, None, None)), b)))) 28 | ).toString 29 | ) 30 | case _ => 31 | Patch.empty 32 | }.asPatch 33 | 34 | /** 35 | * 入れ子構造になった簡略化可能な Match 式を変換すると構文木が破壊されるので、同様のパターンを内部に含まないことを確認する 36 | */ 37 | private def hasMatch(tree: Tree): Boolean = { 38 | // 一つでも見つけたらその後の探索を行う必要はない 39 | var has = false 40 | tree.collect { 41 | case _ if has => // 何もしない 42 | case SimpleReturn(_) => has = true 43 | case HasBlock(_, _, _, _) => has = true 44 | case HasSingleState(_, _, _, _) => has = true 45 | case UnapplySome(_, _, _, _) => has = true 46 | } 47 | has 48 | } 49 | 50 | private def toSingleReplaceIfOnceUseVal(from: Tree, valName: String, rhs: Term, body: Term): Patch = { 51 | body.collect { 52 | case v: Term.Name if v.value == valName => v 53 | } match { 54 | case List(name) => 55 | // String#replace 系のメソッドでは、 "\" が消滅するので "\\" に置換してあげる 56 | Patch.replaceTree(from, body.toString.replaceFirst(name.value, rhs.toString.replaceAll("\\\\", "\\\\\\\\"))) 57 | case _ => toBlockPatch(from, valName, rhs, body) 58 | } 59 | } 60 | 61 | private def toBlockPatch(from: Tree, valName: String, rhs: Term, body: Term): Patch = { 62 | toBlockPatch(from, Defn.Val(Nil, List(Pat.Var(Term.Name(valName))), None, rhs), body) 63 | } 64 | 65 | private def toBlockPatch(from: Tree, defVal: Defn.Val, body: Term): Patch = { 66 | // Block に変換する際、改行を入れないと前の処理の引数として渡されてしまうことがある 67 | body match { 68 | case Term.Block(list) => Patch.replaceTree(from, "\n" + Term.Block(List(defVal) ++ list).toString) 69 | case body => Patch.replaceTree(from, "\n" + Term.Block(List(defVal, body)).toString) 70 | } 71 | } 72 | } 73 | 74 | private object SimpleReturn { 75 | def unapply(tree: Tree): Option[Term.Match] = tree match { 76 | case t @ Match(_, List(Case(Pat.Var(pName), _, b: Term.Name))) if b.value == pName.value => Some(t) 77 | case _ => None 78 | } 79 | } 80 | 81 | private object HasBlock { 82 | def unapply(tree: Tree): Option[(Match, Term.Name, Term, Term.Block)] = tree match { 83 | case t @ Match(expr, List(Case(Pat.Var(pName), _, b: Term.Block))) => Some(t, pName, expr, b) 84 | case _ => None 85 | } 86 | } 87 | 88 | private object HasSingleState { 89 | def unapply(tree: Tree): Option[(Match, Term.Name, Term, Term)] = tree match { 90 | case t @ Match(expr, List(Case(Pat.Var(pName), _, b: Term))) => Some(t, pName, expr, b) 91 | case _ => None 92 | } 93 | } 94 | 95 | private object UnapplySome { 96 | def unapply(tree: Tree): Option[(Match, Term.Name, Term, Term)] = tree match { 97 | case t @ Match(expr, List(Case(Pat.Extract(fun: Term.Name, List(Pat.Var(pName))), _, b: Term))) 98 | if fun.value == "Some" => Some(t, pName, expr, b) 99 | case _ => None 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /rules/src/main/scala/fix/pixiv/UnifiedArrow.scala: -------------------------------------------------------------------------------- 1 | package fix.pixiv 2 | 3 | import scala.meta.tokens.Token 4 | 5 | import scalafix.Patch 6 | import scalafix.v1.{SyntacticDocument, SyntacticRule} 7 | 8 | class UnifiedArrow extends SyntacticRule("UnifiedArrow") { 9 | 10 | override def fix(implicit doc: SyntacticDocument): Patch = { 11 | doc.tokens.map { 12 | case t @ Token.Ident("→") => Patch.replaceToken(t, "->") 13 | case t: Token.LeftArrow => Patch.replaceToken(t, "<-") 14 | case t: Token.RightArrow => Patch.replaceToken(t, "=>") 15 | case _ => Patch.empty 16 | }.asPatch 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /rules/src/main/scala/fix/pixiv/UnifyEmptyList.scala: -------------------------------------------------------------------------------- 1 | package fix.pixiv 2 | 3 | import scala.meta._ 4 | 5 | import scalafix.v1._ 6 | import util.SymbolConverter.SymbolToSemanticType 7 | 8 | class UnifyEmptyList extends SemanticRule("UnifyEmptyList") { 9 | override def fix(implicit doc: SemanticDocument): Patch = { 10 | doc.tree.collect { 11 | case t @ Term.Apply(Term.ApplyType(list @ Term.Name("List"), List(Type.Name(typeVar))), Nil) if isList(list) => 12 | typeVar match { 13 | // List[Nothing]() 14 | case "Nothing" => Patch.replaceTree(t, "Nil") 15 | // List[Any]() 16 | case _ => Patch.replaceTree( 17 | t, 18 | Term.ApplyType(Term.Select(Term.Name("List"), Term.Name("empty")), List(Type.Name(typeVar))).toString() 19 | ) 20 | } 21 | // case List() => 22 | case Case((t @ Pat.Extract(Term.Name("List"), Nil), _, _)) => 23 | Patch.replaceTree(t, "Nil") 24 | // List() 25 | case t @ Term.Apply(list @ Term.Name("List"), Nil) if isList(list) => 26 | Patch.replaceTree(t, "Nil") 27 | case t @ Term.Select(list @ Term.Name("List"), Term.Name("empty")) if isListApply(list) => 28 | t.parent match { 29 | // List.empty[Nothing] 30 | case Some(Term.ApplyType(_, List(Type.Name("Nothing")))) => Patch.replaceTree(t, "Nil") 31 | // List.empty[String] は変換しない 32 | case Some(_: Term.ApplyType) => Patch.empty 33 | // List.empty 34 | case _ => Patch.replaceTree(t, "Nil") 35 | } 36 | }.asPatch 37 | } 38 | 39 | private def isListApply(x1: Term)(implicit doc: SemanticDocument): Boolean = { 40 | try { 41 | x1.symbol.isAssignableTo(List.getClass) 42 | } catch { 43 | case _: Throwable => false 44 | } 45 | } 46 | 47 | private def isList(x1: Term)(implicit doc: SemanticDocument): Boolean = { 48 | try { 49 | x1.symbol.isAssignableTo(classOf[collection.immutable.List[Any]]) 50 | } catch { 51 | case _: Throwable => false 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /rules/src/main/scala/fix/pixiv/UnnecessarySemicolon.scala: -------------------------------------------------------------------------------- 1 | package fix.pixiv 2 | 3 | import scala.meta.tokens.Token 4 | 5 | import scalafix.Patch 6 | import scalafix.v1.{SyntacticDocument, SyntacticRule} 7 | 8 | class UnnecessarySemicolon extends SyntacticRule("UnnecessarySemicolon") { 9 | 10 | override def fix(implicit doc: SyntacticDocument): Patch = { 11 | doc.tokens.zipWithIndex.collect { case (semicolon: Token.Semicolon, i) => 12 | doc.tokens(i + 1) match { 13 | case _ @(Token.CR() | Token.LF() | Token.EOF()) => 14 | Patch.replaceToken(semicolon, "") 15 | case _ => Patch.empty 16 | } 17 | }.asPatch 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /rules/src/main/scala/fix/pixiv/ZeroIndexToHead.scala: -------------------------------------------------------------------------------- 1 | package fix.pixiv 2 | 3 | import scala.meta._ 4 | 5 | import scalafix.v1._ 6 | import util.SymbolConverter.SymbolToSemanticType 7 | 8 | class ZeroIndexToHead extends SemanticRule("ZeroIndexToHead") { 9 | 10 | override def fix(implicit doc: SemanticDocument): Patch = { 11 | doc.tree.collect { 12 | case t @ Term.Apply(Term.Apply(x1, args: List[_]), List(Lit.Int(0))) => 13 | try { 14 | if (x1.symbol.isAssignableTo(Seq.getClass) 15 | || (!x1.symbol.isAssignableTo(List.getClass))) { 16 | Patch.replaceTree( 17 | t, 18 | Term.Select(Term.Apply(x1, args), Term.Name("head")).toString 19 | ) 20 | } else { 21 | Patch.empty 22 | } 23 | } catch { 24 | case _: Throwable => Patch.empty 25 | } 26 | case t @ Term.Apply(x1, List(Lit.Int(0))) => 27 | try { 28 | if (x1.symbol.isAssignableTo(classOf[collection.Seq[Any]]) 29 | && (!x1.symbol.isAssignableTo(classOf[collection.IndexedSeq[Any]]))) { 30 | Patch.replaceTree( 31 | t, 32 | Term.Select(x1, Term.Name("head")).toString 33 | ) 34 | } else { 35 | Patch.empty 36 | } 37 | } catch { 38 | case _: Throwable => Patch.empty 39 | } 40 | }.asPatch 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /rules/src/main/scala/util/SemanticTypeConverter.scala: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import scala.util.Try 4 | 5 | import scalafix.v1._ 6 | 7 | object SemanticTypeConverter { 8 | implicit class SemanticTypeToClass(semanticType: SemanticType) { 9 | @throws[ToClassException] 10 | def toClass: Class[_] = { 11 | semanticType match { 12 | case TypeRef(_, symbol, _) => 13 | symbolToClass(symbol) 14 | case SingleType(_, symbol) => 15 | symbolToClass(symbol) 16 | case ConstantType(constant) => 17 | constantToClass(constant) 18 | case SuperType(_, symbol) => 19 | symbolToClass(symbol) 20 | case ThisType(symbol) => 21 | symbolToClass(symbol) 22 | case _ => 23 | throw new ToClassException(s"${semanticType.toClass} の Class[_] への変換は定義されていません。") 24 | } 25 | } 26 | 27 | def isAssignableFrom(clazz: Class[_]): Boolean = semanticType.toClass.isAssignableFrom(clazz) 28 | 29 | def isAssignableTo(clazz: Class[_]): Boolean = clazz.isAssignableFrom(semanticType.toClass) 30 | 31 | } 32 | 33 | @throws[ToClassException] 34 | def symbolToClass(symbol: scalafix.v1.Symbol): Class[_] = { 35 | val identifierRegStr = "[a-zA-Z$_][a-zA-Z1-9$_]*" 36 | val symbolRegStr = s"($identifierRegStr(?:[/.]$identifierRegStr)*)" 37 | val typeRegStr = s"\\[$identifierRegStr(?:,$identifierRegStr)*]$$" 38 | val classTypeValMatch = (s"^$symbolRegStr#(?:$typeRegStr)?$$").r 39 | val objectTypeValMatch = (s"^$symbolRegStr\\.(?:$typeRegStr)?$$").r 40 | symbol.toString() match { 41 | case classTypeValMatch(str1) => symbolStringToClass( 42 | str1.replace('/', '.') 43 | ) 44 | case objectTypeValMatch(str1) => symbolStringToClass( 45 | // 先に ScalaMeta 側で挿入された . を削除する 46 | str1.replace('/', '.') + "$" 47 | ) 48 | case _ => throw new ToClassException(s"${symbol.toString()} を完全修飾クラス名に変換できませんでした") 49 | } 50 | } 51 | 52 | @throws[ToClassException] 53 | def symbolStringToClass(str: String): Class[_] = { 54 | Try { 55 | Class.forName(str) 56 | }.recover[Class[_]] { 57 | case _ => 58 | // package object 内で type として再代入されている場合には上記の方法では取得できないため、 59 | // 一度コンパニオンオブジェクトを取得してからフィールドとして取り出している。 60 | val lastDot = str.lastIndexOf('.') 61 | val objCls = Class.forName(str.take(lastDot) + "$") 62 | import scala.reflect.runtime.universe.{TypeName, runtimeMirror} 63 | val mirror = runtimeMirror(getClass.getClassLoader) 64 | val clazz: Class[_] = mirror.runtimeClass(mirror.classSymbol(objCls).toType.decl( 65 | TypeName(str.drop(lastDot + 1)) 66 | ).typeSignature.dealias.typeSymbol.asClass) 67 | clazz 68 | }.recover[Class[_]] { 69 | case _: ClassNotFoundException => 70 | throw new ToClassException(s"$str をクラスまたはタイプに変換できませんでした") 71 | case _ => throw new ToClassException("オブジェクトから type を取得できませんでした") 72 | }.get 73 | } 74 | 75 | def constantToClass(constant: Constant): Class[_] = constant match { 76 | case UnitConstant => classOf[Unit] 77 | case BooleanConstant(_) => classOf[Boolean] 78 | case ByteConstant(_) => classOf[Byte] 79 | case ShortConstant(_) => classOf[Short] 80 | case CharConstant(_) => classOf[Char] 81 | case IntConstant(_) => classOf[Int] 82 | case LongConstant(_) => classOf[Long] 83 | case FloatConstant(_) => classOf[Float] 84 | case DoubleConstant(_) => classOf[Double] 85 | case StringConstant(_) => classOf[String] 86 | case NullConstant => classOf[Null] 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /rules/src/main/scala/util/SymbolConverter.scala: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import scalafix.v1._ 4 | import util.SemanticTypeConverter.SemanticTypeToClass 5 | 6 | object SymbolConverter { 7 | implicit class SymbolToSemanticType(symbol: scalafix.v1.Symbol)(implicit doc: Symtab) { 8 | @throws[ToClassException] 9 | def toSemanticType: SemanticType = symbol.info.fold( 10 | throw new ToClassException(s"${symbol.displayName} は info を持ちません。") 11 | ) { info => 12 | info.signature match { 13 | case MethodSignature(_, _, returnType) => 14 | returnType 15 | case ValueSignature(tpe) => 16 | tpe 17 | case TypeSignature(_, lowerBound, upperBound) => 18 | if (lowerBound == upperBound) { 19 | lowerBound 20 | } else if (upperBound != NoType) { 21 | upperBound 22 | } else { 23 | throw new ToClassException( 24 | s"型パラメータ ${symbol.displayName} の型は一意に定まりません。 lower: $lowerBound, upper: $upperBound" 25 | ) 26 | } 27 | case ClassSignature(typeParameters, _, _, _) => 28 | TypeRef( 29 | scalafix.v1.NoType, 30 | symbol, 31 | typeParameters.map(info => SymbolToSemanticType(info.symbol).toSemanticType) 32 | ) 33 | case signature => 34 | throw new ToClassException(s"${symbol.displayName} (${signature.getClass}) は型を持ちません") 35 | } 36 | } 37 | @throws[ToClassException] 38 | def isAssignableFrom(clazz: Class[_]): Boolean = toSemanticType.isAssignableFrom(clazz) 39 | @throws[ToClassException] 40 | def isAssignableTo(clazz: Class[_]): Boolean = toSemanticType.isAssignableTo(clazz) 41 | 42 | } 43 | 44 | } 45 | -------------------------------------------------------------------------------- /rules/src/main/scala/util/ToClassException.scala: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | class ToClassException(message: String) extends RuntimeException(message) 4 | -------------------------------------------------------------------------------- /rules/src/main/scala/util/TreeUtil.scala: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import scala.meta.Tree 4 | 5 | object TreeUtil { 6 | implicit class ShallowCollectTree(tree: Tree) { 7 | def shallowCollect[T](fn: PartialFunction[Tree, Option[T]]): List[T] = { 8 | def traverse(tree: Tree): List[T] = fn(tree) match { 9 | case None => tree.children.flatMap(traverse) 10 | case Some(value) => List(value) 11 | } 12 | traverse(tree) 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /rules/src/test/scala/util/SemanticTypeConverterTest.scala: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import scala.collection.Seq 4 | 5 | import org.scalatest.funsuite.AnyFunSuite 6 | import scalafix.v1.{SingleType, TypeRef} 7 | import util.SemanticTypeConverter.SemanticTypeToClass 8 | 9 | class SemanticTypeConverterTest extends AnyFunSuite { 10 | 11 | test("SemanticTypeToClass: TypeRef") { 12 | assert( 13 | classOf[Seq[_]] == TypeRef(null, scalafix.v1.Symbol("scala/collection/Seq#"), null).toClass 14 | ) 15 | } 16 | 17 | test("SemanticTypeToClass: SingleType") { 18 | assert(classOf[Seq[_]] == SingleType(null, scalafix.v1.Symbol("scala/collection/Seq#")).toClass) 19 | } 20 | 21 | test("isAssignableFrom") { 22 | assert(SingleType(null, scalafix.v1.Symbol("scala/collection/Seq#")).isAssignableFrom(classOf[List[_]])) 23 | } 24 | 25 | test("isAssignableTo") { 26 | assert(SingleType(null, scalafix.v1.Symbol("scala/collection/Seq#")).isAssignableFrom(classOf[Seq[_]])) 27 | } 28 | 29 | test("symbolToClass: クラス名を取得できる") { 30 | assert(classOf[Seq[_]] == SemanticTypeConverter.symbolToClass(scalafix.v1.Symbol("scala/collection/Seq#"))) 31 | } 32 | 33 | test("symbolToClass: オブジェクトを取得できる") { 34 | assert(Seq.getClass == SemanticTypeConverter.symbolToClass(scalafix.v1.Symbol("scala/collection/Seq."))) 35 | } 36 | 37 | test("symbolToClass: 型パラメータを持つクラス名を取得できる") { 38 | assert( 39 | classOf[List[_]] == SemanticTypeConverter.symbolToClass(scalafix.v1.Symbol("scala/collection/immutable/List#[T]")) 40 | ) 41 | } 42 | 43 | test("symbolStringToClass: クラス名を取得できる") { 44 | assert(classOf[Seq[_]] == SemanticTypeConverter.symbolStringToClass("scala.collection.Seq")) 45 | } 46 | 47 | test("symbolStringToClass: type で再定義された型からも取得できる") { 48 | assert(classOf[java.lang.String] == SemanticTypeConverter.symbolStringToClass("scala.Predef.String")) 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /tests/src/test/scala/fix/RuleSuite.scala: -------------------------------------------------------------------------------- 1 | package fix 2 | 3 | import scalafix.testkit._ 4 | import org.scalatest.funsuite.AnyFunSuiteLike 5 | 6 | class RuleSuite extends AbstractSemanticRuleSuite with AnyFunSuiteLike { 7 | runAllTests() 8 | } 9 | --------------------------------------------------------------------------------