Skip to content

Commit 3ea4f8b

Browse files
committed
type-based ScalacOption [still in progress]
1 parent bb9e392 commit 3ea4f8b

File tree

5 files changed

+297
-0
lines changed

5 files changed

+297
-0
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/*
2+
* Copyright 2022 Typelevel
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.typelevel.scalacoptions.proposal
18+
19+
// TODO: TBD
20+
sealed abstract class ParseFailure
21+
22+
object ParseFailure {}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
/*
2+
* Copyright 2022 Typelevel
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.typelevel.scalacoptions.proposal
18+
19+
import org.typelevel.scalacoptions.ScalaVersion
20+
21+
trait ScalacOption[A] {
22+
type Type <: ScalacOption.Type
23+
24+
// ..or `isSupportedFor` maybe?
25+
def isSupported(sv: ScalaVersion): Boolean
26+
27+
/** Base option name.
28+
*
29+
* @return
30+
* - for `"-deprecation"` results to `"deprecation"`
31+
* - for `"-deprecation:true"` results to `"deprecation"`
32+
* - for `"-Xlint"` results to `"Xlint"`
33+
* - for `"-Xlint:deprecation"` results to `"Xlint"`
34+
*/
35+
def baseName: String
36+
37+
// /** Full option name.
38+
// *
39+
// * @return
40+
// * - for `"-deprecation"` results to `"deprecation"`
41+
// * - for `"-deprecation:true"` results to `"deprecation"` (same as above!)
42+
// * - for `"-Xlint"` results to `"Xlint"`
43+
// * - for `"-Xlint:deprecation"` results to `"Xlint:deprecation"`
44+
// */
45+
// def fullName: String
46+
47+
/** Parses the option from a raw value found for the option's base name.
48+
*
49+
* @param rawValue
50+
* a raw option value, can be empty strings for options like `-deprecation`
51+
*
52+
* @return
53+
* - `None` if the value does not belong to the option and thus cannot be parsed, i.e. when
54+
* `"unused"` is passed where `-Xlint:deprecation` model is expected;
55+
* - `Some(A)` when a correct value is passed for the option, e.g. both `"unused"` and
56+
* `"-unused"` will be accepted for the an option type class that models the `-Xlint:unused`
57+
* option.
58+
*
59+
* @note
60+
* It deliberately made not returning any failure type like `ParseFailure`, becase
61+
* `ScalacOption.Select` is responsible for that.
62+
*/
63+
def parse(rawValue: String): Option[A]
64+
}
65+
66+
object ScalacOption {
67+
type Aux[A, T <: Type] = ScalacOption[A] { type Type = T }
68+
69+
// Note: initially I thought that Singe/Recurring might not be necessary.
70+
// But later I realized that `Recurring` can be useful for modeling such options like "-Wconf"
71+
sealed trait Type
72+
abstract final class Single private () extends Type // never instantiated
73+
abstract final class Recurring private () extends Type // never instantiated
74+
75+
sealed trait Select[A] {
76+
// Not sure why it is a higher kinded type in http4s' Header.
77+
// A regular plain type seems to be just enough here.
78+
type Out
79+
80+
/** Parses all raw option values groupped by their [[ScalacOption.baseName]].
81+
*
82+
* @param rawValues
83+
* a list of raw option values in the order they occured in a command line.
84+
*/
85+
def parse(rawValues: List[String]): Option[Out]
86+
}
87+
88+
object Select {
89+
implicit def singleScalacOption[A](implicit
90+
so: ScalacOption.Aux[A, Single]
91+
): Select[A] { type Out = A } =
92+
new Select[A] {
93+
type Out = A
94+
95+
def parse(rawValues: List[String]): Option[A] = {
96+
// Assume for now that for a single-occuring option the last value occured overrides
97+
// all previous values. E.g.: for `Seq("-feature", "-feature:false")` the last one should
98+
// take effect.
99+
rawValues.iterator.flatMap(so.parse).toList.lastOption
100+
}
101+
}
102+
103+
implicit def recurringScalacOption[A](implicit
104+
so: ScalacOption.Aux[A, Recurring]
105+
): Select[A] { type Out = List[A] } =
106+
new Select[A] {
107+
type Out = List[A] // consider NonEmptyList
108+
109+
def parse(rawValues: List[String]): Option[List[A]] = {
110+
// As simple as it is (but with NonEmptyList it would be even simpler).
111+
rawValues.iterator.flatMap(so.parse).toList match {
112+
case Nil => None
113+
case nel => Some(nel) // the result cannot be an empty list!
114+
}
115+
}
116+
}
117+
}
118+
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
/*
2+
* Copyright 2022 Typelevel
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.typelevel.scalacoptions.proposal
18+
19+
/** A collection of scalac options.
20+
*
21+
* @param rawOptions
22+
* raw untyped scalac options groupped by their base name.
23+
*
24+
* @example
25+
* The following list of options:
26+
* {{{
27+
* Seq(
28+
* "-deprecation",
29+
* "-feature:false",
30+
* "-Xlint:deprecation,unused",
31+
* "-Wconf:cat=lint&msg=hello:e,any:i",
32+
* "-Xlint:_,-constant",
33+
* "-Wunused"
34+
* )
35+
* }}}
36+
* will be represented as:
37+
* {{{
38+
* Map(
39+
* "deprecation" -> List(""),
40+
* "feature" -> List("false"),
41+
* "Xlint" -> List("deprecation", "unused", "_", "-constant"),
42+
* "Wconf" -> List("msg=cat=lint&msg=hello", "any:i"),
43+
* "Wunused" -> List("")
44+
* )
45+
* }}}
46+
*
47+
* @note
48+
* Even a single option like `-Xlint` should be kept as a pair of `"Xlint" -> List("")`, because
49+
* it can be important to parse groupped options correctly. It means that the underlying list
50+
* cannot be empty. Thus, the question: can be consider `NonEmptyList` from cats for that?
51+
*/
52+
final class ScalacOptions(rawOptions: Map[String, List[String]]) {
53+
54+
def get[A](implicit opt: ScalacOption[A], sel: ScalacOption.Select[A]): Option[sel.Out] = {
55+
rawOptions
56+
.get(opt.baseName)
57+
.flatMap { sel.parse(_) }
58+
}
59+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright 2022 Typelevel
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.typelevel.scalacoptions.proposal
18+
19+
class ScalacOptionSuite {}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/*
2+
* Copyright 2022 Typelevel
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.typelevel.scalacoptions.proposal
18+
19+
import org.scalacheck.Arbitrary
20+
import org.scalacheck.Gen
21+
import org.scalacheck.Prop
22+
import org.typelevel.scalacoptions.ScalaVersion
23+
24+
class ScalacOptionsSuite extends munit.ScalaCheckSuite {
25+
import ScalacOptionsSuite._
26+
27+
private val arbStrGen = Arbitrary.arbitrary[String]
28+
// uncomment to simplify debugging
29+
// TODO: remove!
30+
// private val arbStrGen = Gen.asciiPrintableStr
31+
32+
test("ScalacOptions.get should work for ScalacOption.Single") {
33+
val gen =
34+
for {
35+
targetBaseName <- arbStrGen
36+
targetResultValue <- arbStrGen
37+
targetOtherValues <-
38+
Gen.listOf(
39+
Gen.oneOf(
40+
arbStrGen,
41+
arbStrGen.map(targetResultValue + _)
42+
)
43+
)
44+
otherOptions <- Gen.mapOf(Gen.zip(arbStrGen, Gen.listOf(arbStrGen)))
45+
} yield {
46+
(
47+
targetBaseName,
48+
targetResultValue,
49+
otherOptions + (targetBaseName -> (targetOtherValues :+ targetResultValue))
50+
)
51+
}
52+
53+
Prop.forAll(gen) { case (targetBaseName, targetResultValue, allOptions) =>
54+
implicit val singleScalacOption: ScalacOption.Aux[TestOption, ScalacOption.Single] =
55+
new ScalacOption[TestOption] {
56+
type Type = ScalacOption.Single
57+
58+
override def isSupported(sv: ScalaVersion): Boolean = ??? // not a subject for testing
59+
60+
override def baseName: String = targetBaseName
61+
62+
override def parse(rawValue: String): Option[TestOption] = {
63+
if (rawValue.startsWith(targetResultValue))
64+
Some(TestOption(rawValue))
65+
else
66+
None
67+
}
68+
}
69+
70+
val obtained = new ScalacOptions(allOptions).get[TestOption].map(_.value)
71+
72+
assertEquals(obtained, Some(targetResultValue))
73+
}
74+
}
75+
}
76+
77+
object ScalacOptionsSuite {
78+
final case class TestOption(value: String) extends AnyVal
79+
}

0 commit comments

Comments
 (0)