├── .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 | 
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 | 
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 |
--------------------------------------------------------------------------------