-
Notifications
You must be signed in to change notification settings - Fork 12
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[proposal] ScalacOption class based on types #18
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 {} |
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] = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I imagine this method will need to delimit the scalac options without knowing all of them. One easy algorithm is to treat every There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would say that a more reliable (and performant, actually) way could be pre-parsing all the options at once, and then just picking them by one from the collection of parsed options upon a user request. But it would require a global "registry" of parsers. Which is not that bad, actually, but makes the approach different from how Headers are treated. The root cause of the difference is kinda "fundamental" – in regards to headers there're RFCs that clearly define how to separate various headers from each other without digging into their contents. But for CLI options there's no such a clear way of separating them 🤷 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I agree it would be much more reliable, but I would very much like to avoid this for fear this library will completely stop working if it encounters an unknown scalac option. It must be able to cleanly pass-through anything it cannot recognize. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right, we definitely should try to make the parser more lenient. It would be a good trade-off between reliability and tolerance. |
||
// 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! | ||
} | ||
} | ||
} | ||
} | ||
} |
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(_) } | ||
} | ||
} |
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 {} |
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 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, I think it should be
rawValue: Seq[String]