From 145dc2481e7eaeb68cf159f2ccd149c955f564fd Mon Sep 17 00:00:00 2001 From: satorg Date: Fri, 7 Oct 2022 03:03:35 -0700 Subject: [PATCH] type-based ScalacOption [still in progress] --- .../scalacoptions/proposal/ParseFailure.scala | 22 ++++ .../scalacoptions/proposal/ScalacOption.scala | 118 ++++++++++++++++++ .../proposal/ScalacOptions.scala | 59 +++++++++ .../proposal/ScalacOptionSuite.scala | 19 +++ .../proposal/ScalacOptionsSuite.scala | 79 ++++++++++++ 5 files changed, 297 insertions(+) create mode 100644 lib/src/main/scala/org/typelevel/scalacoptions/proposal/ParseFailure.scala create mode 100644 lib/src/main/scala/org/typelevel/scalacoptions/proposal/ScalacOption.scala create mode 100644 lib/src/main/scala/org/typelevel/scalacoptions/proposal/ScalacOptions.scala create mode 100644 lib/src/test/scala/org/typelevel/scalacoptions/proposal/ScalacOptionSuite.scala create mode 100644 lib/src/test/scala/org/typelevel/scalacoptions/proposal/ScalacOptionsSuite.scala diff --git a/lib/src/main/scala/org/typelevel/scalacoptions/proposal/ParseFailure.scala b/lib/src/main/scala/org/typelevel/scalacoptions/proposal/ParseFailure.scala new file mode 100644 index 0000000..5f19894 --- /dev/null +++ b/lib/src/main/scala/org/typelevel/scalacoptions/proposal/ParseFailure.scala @@ -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 {} diff --git a/lib/src/main/scala/org/typelevel/scalacoptions/proposal/ScalacOption.scala b/lib/src/main/scala/org/typelevel/scalacoptions/proposal/ScalacOption.scala new file mode 100644 index 0000000..a43bdab --- /dev/null +++ b/lib/src/main/scala/org/typelevel/scalacoptions/proposal/ScalacOption.scala @@ -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! + } + } + } + } +} diff --git a/lib/src/main/scala/org/typelevel/scalacoptions/proposal/ScalacOptions.scala b/lib/src/main/scala/org/typelevel/scalacoptions/proposal/ScalacOptions.scala new file mode 100644 index 0000000..0150dee --- /dev/null +++ b/lib/src/main/scala/org/typelevel/scalacoptions/proposal/ScalacOptions.scala @@ -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(_) } + } +} diff --git a/lib/src/test/scala/org/typelevel/scalacoptions/proposal/ScalacOptionSuite.scala b/lib/src/test/scala/org/typelevel/scalacoptions/proposal/ScalacOptionSuite.scala new file mode 100644 index 0000000..4099eb2 --- /dev/null +++ b/lib/src/test/scala/org/typelevel/scalacoptions/proposal/ScalacOptionSuite.scala @@ -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 {} diff --git a/lib/src/test/scala/org/typelevel/scalacoptions/proposal/ScalacOptionsSuite.scala b/lib/src/test/scala/org/typelevel/scalacoptions/proposal/ScalacOptionsSuite.scala new file mode 100644 index 0000000..1a77caf --- /dev/null +++ b/lib/src/test/scala/org/typelevel/scalacoptions/proposal/ScalacOptionsSuite.scala @@ -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 +}