-
Notifications
You must be signed in to change notification settings - Fork 12
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
type-based ScalacOption [still in progress]
- Loading branch information
Showing
5 changed files
with
297 additions
and
0 deletions.
There are no files selected for viewing
22 changes: 22 additions & 0 deletions
22
lib/src/main/scala/org/typelevel/scalacoptions/proposal/ParseFailure.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
/* | ||
* Copyright 2022 Typelevel | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package org.typelevel.scalacoptions.proposal | ||
|
||
// TODO: TBD | ||
sealed abstract class ParseFailure | ||
|
||
object ParseFailure {} |
118 changes: 118 additions & 0 deletions
118
lib/src/main/scala/org/typelevel/scalacoptions/proposal/ScalacOption.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
/* | ||
* Copyright 2022 Typelevel | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package org.typelevel.scalacoptions.proposal | ||
|
||
import org.typelevel.scalacoptions.ScalaVersion | ||
|
||
trait ScalacOption[A] { | ||
type Type <: ScalacOption.Type | ||
|
||
// ..or `isSupportedFor` maybe? | ||
def isSupported(sv: ScalaVersion): Boolean | ||
|
||
/** Base option name. | ||
* | ||
* @return | ||
* - for `"-deprecation"` results to `"deprecation"` | ||
* - for `"-deprecation:true"` results to `"deprecation"` | ||
* - for `"-Xlint"` results to `"Xlint"` | ||
* - for `"-Xlint:deprecation"` results to `"Xlint"` | ||
*/ | ||
def baseName: String | ||
|
||
// /** Full option name. | ||
// * | ||
// * @return | ||
// * - for `"-deprecation"` results to `"deprecation"` | ||
// * - for `"-deprecation:true"` results to `"deprecation"` (same as above!) | ||
// * - for `"-Xlint"` results to `"Xlint"` | ||
// * - for `"-Xlint:deprecation"` results to `"Xlint:deprecation"` | ||
// */ | ||
// def fullName: String | ||
|
||
/** Parses the option from a raw value found for the option's base name. | ||
* | ||
* @param rawValue | ||
* a raw option value, can be empty strings for options like `-deprecation` | ||
* | ||
* @return | ||
* - `None` if the value does not belong to the option and thus cannot be parsed, i.e. when | ||
* `"unused"` is passed where `-Xlint:deprecation` model is expected; | ||
* - `Some(A)` when a correct value is passed for the option, e.g. both `"unused"` and | ||
* `"-unused"` will be accepted for the an option type class that models the `-Xlint:unused` | ||
* option. | ||
* | ||
* @note | ||
* It deliberately made not returning any failure type like `ParseFailure`, becase | ||
* `ScalacOption.Select` is responsible for that. | ||
*/ | ||
def parse(rawValue: String): Option[A] | ||
} | ||
|
||
object ScalacOption { | ||
type Aux[A, T <: Type] = ScalacOption[A] { type Type = T } | ||
|
||
// Note: initially I thought that Singe/Recurring might not be necessary. | ||
// But later I realized that `Recurring` can be useful for modeling such options like "-Wconf" | ||
sealed trait Type | ||
abstract final class Single private () extends Type // never instantiated | ||
abstract final class Recurring private () extends Type // never instantiated | ||
|
||
sealed trait Select[A] { | ||
// Not sure why it is a higher kinded type in http4s' Header. | ||
// A regular plain type seems to be just enough here. | ||
type Out | ||
|
||
/** Parses all raw option values groupped by their [[ScalacOption.baseName]]. | ||
* | ||
* @param rawValues | ||
* a list of raw option values in the order they occured in a command line. | ||
*/ | ||
def parse(rawValues: List[String]): Option[Out] | ||
} | ||
|
||
object Select { | ||
implicit def singleScalacOption[A](implicit | ||
so: ScalacOption.Aux[A, Single] | ||
): Select[A] { type Out = A } = | ||
new Select[A] { | ||
type Out = A | ||
|
||
def parse(rawValues: List[String]): Option[A] = { | ||
// Assume for now that for a single-occuring option the last value occured overrides | ||
// all previous values. E.g.: for `Seq("-feature", "-feature:false")` the last one should | ||
// take effect. | ||
rawValues.iterator.flatMap(so.parse).toList.lastOption | ||
} | ||
} | ||
|
||
implicit def recurringScalacOption[A](implicit | ||
so: ScalacOption.Aux[A, Recurring] | ||
): Select[A] { type Out = List[A] } = | ||
new Select[A] { | ||
type Out = List[A] // consider NonEmptyList | ||
|
||
def parse(rawValues: List[String]): Option[List[A]] = { | ||
// As simple as it is (but with NonEmptyList it would be even simpler). | ||
rawValues.iterator.flatMap(so.parse).toList match { | ||
case Nil => None | ||
case nel => Some(nel) // the result cannot be an empty list! | ||
} | ||
} | ||
} | ||
} | ||
} |
59 changes: 59 additions & 0 deletions
59
lib/src/main/scala/org/typelevel/scalacoptions/proposal/ScalacOptions.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
/* | ||
* Copyright 2022 Typelevel | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package org.typelevel.scalacoptions.proposal | ||
|
||
/** A collection of scalac options. | ||
* | ||
* @param rawOptions | ||
* raw untyped scalac options groupped by their base name. | ||
* | ||
* @example | ||
* The following list of options: | ||
* {{{ | ||
* Seq( | ||
* "-deprecation", | ||
* "-feature:false", | ||
* "-Xlint:deprecation,unused", | ||
* "-Wconf:cat=lint&msg=hello:e,any:i", | ||
* "-Xlint:_,-constant", | ||
* "-Wunused" | ||
* ) | ||
* }}} | ||
* will be represented as: | ||
* {{{ | ||
* Map( | ||
* "deprecation" -> List(""), | ||
* "feature" -> List("false"), | ||
* "Xlint" -> List("deprecation", "unused", "_", "-constant"), | ||
* "Wconf" -> List("msg=cat=lint&msg=hello", "any:i"), | ||
* "Wunused" -> List("") | ||
* ) | ||
* }}} | ||
* | ||
* @note | ||
* Even a single option like `-Xlint` should be kept as a pair of `"Xlint" -> List("")`, because | ||
* it can be important to parse groupped options correctly. It means that the underlying list | ||
* cannot be empty. Thus, the question: can be consider `NonEmptyList` from cats for that? | ||
*/ | ||
final class ScalacOptions(rawOptions: Map[String, List[String]]) { | ||
|
||
def get[A](implicit opt: ScalacOption[A], sel: ScalacOption.Select[A]): Option[sel.Out] = { | ||
rawOptions | ||
.get(opt.baseName) | ||
.flatMap { sel.parse(_) } | ||
} | ||
} |
19 changes: 19 additions & 0 deletions
19
lib/src/test/scala/org/typelevel/scalacoptions/proposal/ScalacOptionSuite.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
/* | ||
* Copyright 2022 Typelevel | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package org.typelevel.scalacoptions.proposal | ||
|
||
class ScalacOptionSuite {} |
79 changes: 79 additions & 0 deletions
79
lib/src/test/scala/org/typelevel/scalacoptions/proposal/ScalacOptionsSuite.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
/* | ||
* Copyright 2022 Typelevel | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package org.typelevel.scalacoptions.proposal | ||
|
||
import org.scalacheck.Arbitrary | ||
import org.scalacheck.Gen | ||
import org.scalacheck.Prop | ||
import org.typelevel.scalacoptions.ScalaVersion | ||
|
||
class ScalacOptionsSuite extends munit.ScalaCheckSuite { | ||
import ScalacOptionsSuite._ | ||
|
||
private val arbStrGen = Arbitrary.arbitrary[String] | ||
// uncomment to simplify debugging | ||
// TODO: remove! | ||
// private val arbStrGen = Gen.asciiPrintableStr | ||
|
||
test("ScalacOptions.get should work for ScalacOption.Single") { | ||
val gen = | ||
for { | ||
targetBaseName <- arbStrGen | ||
targetResultValue <- arbStrGen | ||
targetOtherValues <- | ||
Gen.listOf( | ||
Gen.oneOf( | ||
arbStrGen, | ||
arbStrGen.map(targetResultValue + _) | ||
) | ||
) | ||
otherOptions <- Gen.mapOf(Gen.zip(arbStrGen, Gen.listOf(arbStrGen))) | ||
} yield { | ||
( | ||
targetBaseName, | ||
targetResultValue, | ||
otherOptions + (targetBaseName -> (targetOtherValues :+ targetResultValue)) | ||
) | ||
} | ||
|
||
Prop.forAll(gen) { case (targetBaseName, targetResultValue, allOptions) => | ||
implicit val singleScalacOption: ScalacOption.Aux[TestOption, ScalacOption.Single] = | ||
new ScalacOption[TestOption] { | ||
type Type = ScalacOption.Single | ||
|
||
override def isSupported(sv: ScalaVersion): Boolean = ??? // not a subject for testing | ||
|
||
override def baseName: String = targetBaseName | ||
|
||
override def parse(rawValue: String): Option[TestOption] = { | ||
if (rawValue.startsWith(targetResultValue)) | ||
Some(TestOption(rawValue)) | ||
else | ||
None | ||
} | ||
} | ||
|
||
val obtained = new ScalacOptions(allOptions).get[TestOption].map(_.value) | ||
|
||
assertEquals(obtained, Some(targetResultValue)) | ||
} | ||
} | ||
} | ||
|
||
object ScalacOptionsSuite { | ||
final case class TestOption(value: String) extends AnyVal | ||
} |