diff --git a/.github/workflows/sbt.yml b/.github/workflows/sbt.yml index a1456afd..c0486768 100644 --- a/.github/workflows/sbt.yml +++ b/.github/workflows/sbt.yml @@ -15,7 +15,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, windows-latest] - scalajs: ["1.9.0"] + scalajs: ["1.16.0"] es2015_enabled: ["false", "true"] steps: - name: Configure git to disable Windows line feeds diff --git a/build.sbt b/build.sbt index 6eeb986f..95f51429 100644 --- a/build.sbt +++ b/build.sbt @@ -8,13 +8,13 @@ addCommandAlias( "compile:scalafix --check; test:scalafix --check; compile:scalafmtCheck; test:scalafmtCheck; scalafmtSbtCheck" ) -val scala212 = "2.12.17" -val scala213 = "2.13.10" -val scala3 = "3.2.2" +val scala212 = "2.12.19" +val scala213 = "2.13.14" +val scala3 = "3.3.3" ThisBuild / scalaVersion := scala213 ThisBuild / semanticdbEnabled := true -ThisBuild / semanticdbVersion := "4.7.6" +ThisBuild / semanticdbVersion := "4.9.9" ThisBuild / tpolecatDefaultOptionsMode := DevMode @@ -46,6 +46,7 @@ addCommandAlias( lazy val crossScalaSettings = Seq( crossScalaVersions := Seq(scala212, scala213, scala3), + scalacOptions += "-Wconf:cat=unused-nowarn:s", Compile / unmanagedSourceDirectories ++= { val sourceDir = (Compile / sourceDirectory).value CrossVersion.partialVersion(scalaVersion.value) match { @@ -210,7 +211,7 @@ lazy val docs = project.settings(librarySettings, macroAnnotationSettings).dependsOn(web, hot, docsMacros, history) ThisBuild / updateIntellij := {} -val intelliJVersion = "231.8109.175" // 2023.1 +val intelliJVersion = "242.20224.300" // 2023.2 lazy val coreIntellijSupport = project.settings( org.jetbrains.sbtidea.Keys.buildSettings :+ ( diff --git a/core/src/main/scala-2/slinky/core/annotations/react.scala b/core/src/main/scala-2/slinky/core/annotations/react.scala index b9b37549..a10c2939 100644 --- a/core/src/main/scala-2/slinky/core/annotations/react.scala +++ b/core/src/main/scala-2/slinky/core/annotations/react.scala @@ -64,7 +64,19 @@ object ReactMacrosImpl { q"this.apply(Props.apply[..$tparams](...$applyValues))" } - q"""def apply[..$tparams](...$paramssWithoutChildren)(${childrenParam.get}): _root_.slinky.core.KeyAndRefAddingStage[Def] = + val children = { + val tpe = childrenParam.get.tpe + if (tpe != null && (tpe =:= typeOf[Seq[_root_.slinky.core.facade.ReactElement]] || + tpe =:= typeOf[List[_root_.slinky.core.facade.ReactElement]] || + (tpe.typeSymbol == definitions.RepeatedParamClass && + tpe.typeArgs.headOption.exists(_ =:= typeOf[_root_.slinky.core.facade.ReactElement])))) { + q"${childrenParam.get.name}: _root_.slinky.core.facade.ReactElement_*" + } else { + q"${childrenParam.get}" + } + } + + q"""def apply[..$tparams](...$paramssWithoutChildren)($children): _root_.slinky.core.KeyAndRefAddingStage[Def] = $body""" } else { q"""def apply[..$tparams](...$paramssWithoutChildren): _root_.slinky.core.KeyAndRefAddingStage[Def] = diff --git a/coreIntellijSupport/src/main/scala/slinky/core/SlinkyInjector.scala b/coreIntellijSupport/src/main/scala/slinky/core/SlinkyInjector.scala index 69a69576..e2d10536 100644 --- a/coreIntellijSupport/src/main/scala/slinky/core/SlinkyInjector.scala +++ b/coreIntellijSupport/src/main/scala/slinky/core/SlinkyInjector.scala @@ -1,79 +1,100 @@ package slinky.core -import org.jetbrains.plugins.scala.lang.psi.types.ScParameterizedType -import org.jetbrains.plugins.scala.lang.psi.impl.toplevel.typedef.{SyntheticMembersInjector, TypeDefinitionMembers} +import com.intellij.psi.{PsiClassType, PsiType} +import com.intellij.psi.util.PsiTypesUtil +import org.jetbrains.plugins.scala.extensions.PsiClassExt +import org.jetbrains.plugins.scala.lang.psi.api.statements.{ + ScPatternDefinition, + ScTypeAliasDefinition, + ScValueDeclaration +} import org.jetbrains.plugins.scala.lang.psi.api.toplevel.typedef._ -import org.jetbrains.plugins.scala.lang.psi.api.statements.{ScTypeAliasDefinition, ScValueDeclaration, ScPatternDefinition} +import org.jetbrains.plugins.scala.lang.psi.impl.toplevel.typedef.{SyntheticMembersInjector, TypeDefinitionMembers} +import org.jetbrains.plugins.scala.lang.psi.types.ScParameterizedType class SlinkyInjector extends SyntheticMembersInjector { sealed trait InjectType case object Function extends InjectType - case object Type extends InjectType - case object Member extends InjectType + case object Type extends InjectType + case object Member extends InjectType - override def needsCompanionObject(source: ScTypeDefinition): Boolean = { + override def needsCompanionObject(source: ScTypeDefinition): Boolean = source.findAnnotationNoAliases("slinky.core.annotations.react") != null - } def createComponentBody(cls: ScTypeDefinition): Seq[(String, InjectType)] = { val types = TypeDefinitionMembers.getTypes(cls) - val (propsDefinition, applyMethods) = types.forName("Props").iterator.toSeq.headOption.flatMap { elm => - elm.namedElement match { - case alias: ScTypeAliasDefinition => - Some(((alias.getText, Member), Seq.empty[(String, InjectType)])) - case propsCls: ScClass if propsCls.isCase => - Some(((propsCls.getText, Type), { - val paramList = propsCls.constructor.get.parameterList - val caseClassparamss = paramList.params - val childrenParam = caseClassparamss.find(_.name == "children") - - val paramssWithoutChildren = caseClassparamss.filterNot(childrenParam.contains) - - if (childrenParam.isDefined) { - if (paramssWithoutChildren.isEmpty) { - Seq( - s"def apply(${childrenParam.get.getText}): slinky.core.KeyAndRefAddingStage[${cls.getQualifiedName}] = ???" -> Function - ) + val (propsDefinition, applyMethods) = types + .forName("Props") + .iterator + .toSeq + .headOption + .flatMap { elm => + elm.namedElement match { + case alias: ScTypeAliasDefinition => + Some(((alias.getText, Member), Seq.empty[(String, InjectType)])) + case propsCls: ScClass if propsCls.isCase => + Some(((propsCls.getText, Type), { + val paramList = propsCls.constructor.get.parameterList + val caseClassparamss = paramList.params + val childrenParam = caseClassparamss.find(_.name == "children") + + val paramssWithoutChildren = caseClassparamss.filterNot(childrenParam.contains) + + if (childrenParam.isDefined) { + if (paramssWithoutChildren.isEmpty) { + Seq( + s"def apply(${childrenParam.get.getText}): slinky.core.KeyAndRefAddingStage[${cls.getQualifiedName}] = ???" -> Function + ) + } else { + val children = childrenParam.get.getType() match { + case tpe if isChildrenReactElement(tpe) => + s"${childrenParam.get.getName}: slinky.core.facade.ReactElement*" + case _ => + childrenParam.get.getText + } + + Seq( + s"def apply(${paramssWithoutChildren.map(_.getText).mkString(",")})($children): slinky.core.KeyAndRefAddingStage[${cls.getQualifiedName}] = ???" -> Function + ) + } } else { Seq( - s"def apply(${paramssWithoutChildren.map(_.getText).mkString(",")})(${childrenParam.get.getText}): slinky.core.KeyAndRefAddingStage[${cls.getQualifiedName}] = ???" -> Function + s"def apply(${paramssWithoutChildren.map(_.getText).mkString(",")}): slinky.core.KeyAndRefAddingStage[${cls.getQualifiedName}] = ???" -> Function ) } - } else { - Seq( - s"def apply(${paramssWithoutChildren.map(_.getText).mkString(",")}): slinky.core.KeyAndRefAddingStage[${cls.getQualifiedName}] = ???" -> Function - ) - } - })) - case _ => None - } - }.getOrElse((("", Type), Seq.empty)) - - val stateDefinition: Option[(String, InjectType)] = types.forName("State").iterator.toSeq.headOption.flatMap { elm => - elm.namedElement match { - case alias: ScTypeAliasDefinition => - Some((alias.getText, Member)) - case propsCls: ScClass if propsCls.isCase => - Some((propsCls.getText, Type)) - case _ => None + })) + case _ => None + } } + .getOrElse((("", Type), Seq.empty)) + + val stateDefinition: Option[(String, InjectType)] = types.forName("State").iterator.toSeq.headOption.flatMap { + elm => + elm.namedElement match { + case alias: ScTypeAliasDefinition => + Some((alias.getText, Member)) + case propsCls: ScClass if propsCls.isCase => + Some((propsCls.getText, Type)) + case _ => None + } } - val snapshotDefinition: Option[(String, InjectType)] = types.forName("Snapshot").iterator.toSeq.headOption.flatMap { elm => - elm.namedElement match { - case alias: ScTypeAliasDefinition => - Some((alias.getText, Member)) - case propsCls: ScClass if propsCls.isCase => - Some((propsCls.getText, Type)) - case _ => None - } + val snapshotDefinition: Option[(String, InjectType)] = types.forName("Snapshot").iterator.toSeq.headOption.flatMap { + elm => + elm.namedElement match { + case alias: ScTypeAliasDefinition => + Some((alias.getText, Member)) + case propsCls: ScClass if propsCls.isCase => + Some((propsCls.getText, Type)) + case _ => None + } } (s"type Def = ${cls.getQualifiedName}", Member) +: propsDefinition +: stateDefinition.getOrElse(("type State = Unit", Member)) +: (snapshotDefinition.toList ++ - applyMethods) + applyMethods) } def elementAndRefType(external: ScTypeDefinition) = { @@ -100,102 +121,128 @@ class SlinkyInjector extends SyntheticMembersInjector { def createExternalBody(cls: ScTypeDefinition): Seq[(String, InjectType)] = { val applyMethods = cls.extendsBlock.members.collect { case td: ScTypeDefinition => td - }.find(_.name == "Props").map { - case propsCls: ScClass if propsCls.isCase => - val (element, ref) = elementAndRefType(cls) - val paramList = propsCls.constructor.get.parameterList - - if (paramList.params.forall(_.isDefaultParam)) { - Seq( - s"def apply${paramList.getText}: _root_.slinky.core.BuildingComponent[$element, $ref] = ???" -> Function, - s"def apply(mod: _root_.slinky.core.AttrPair[$element], tagMods: _root_.slinky.core.AttrPair[$element]*): _root_.slinky.core.BuildingComponent[$element, $ref] = ???" -> Function, - s"def withKey(key: String): _root_.slinky.core.BuildingComponent[$element, $ref] = ???" -> Function, - s"def withRef(ref: $ref => Unit): _root_.slinky.core.BuildingComponent[$element, $ref] = ???" -> Function, - s"def withRef(ref: _root_.slinky.core.facade.ReactRef[$ref]): _root_.slinky.core.BuildingComponent[$element, $ref] = ???" -> Function, - s"def apply(children: _root_.slinky.core.facade.ReactElement*): _root_.slinky.core.BuildingComponent[$element, $ref] = ???" -> Function - ) - } else { - Seq( - s"def apply${paramList.getText}: slinky.core.BuildingComponent[$element, $ref] = ???" -> Function - ) - } + }.find(_.name == "Props") + .map { + case propsCls: ScClass if propsCls.isCase => + val (element, ref) = elementAndRefType(cls) + val paramList = propsCls.constructor.get.parameterList + + if (paramList.params.forall(_.isDefaultParam)) { + Seq( + s"def apply${paramList.getText}: _root_.slinky.core.BuildingComponent[$element, $ref] = ???" -> Function, + s"def apply(mod: _root_.slinky.core.AttrPair[$element], tagMods: _root_.slinky.core.AttrPair[$element]*): _root_.slinky.core.BuildingComponent[$element, $ref] = ???" -> Function, + s"def withKey(key: String): _root_.slinky.core.BuildingComponent[$element, $ref] = ???" -> Function, + s"def withRef(ref: $ref => Unit): _root_.slinky.core.BuildingComponent[$element, $ref] = ???" -> Function, + s"def withRef(ref: _root_.slinky.core.facade.ReactRef[$ref]): _root_.slinky.core.BuildingComponent[$element, $ref] = ???" -> Function, + s"def apply(children: _root_.slinky.core.facade.ReactElement*): _root_.slinky.core.BuildingComponent[$element, $ref] = ???" -> Function + ) + } else { + Seq( + s"def apply${paramList.getText}: slinky.core.BuildingComponent[$element, $ref] = ???" -> Function + ) + } - case _ => Seq.empty - }.getOrElse(Seq.empty) + case _ => Seq.empty + } + .getOrElse(Seq.empty) applyMethods } def createFunctionalComponentBody(cls: ScTypeDefinition): Seq[(String, InjectType)] = { val applyMethods = cls.extendsBlock.members.collect { - case td: ScClass => td + case td: ScClass => td case td: ScTypeAliasDefinition => td - }.find(_.name == "Props").flatMap { elm => - elm match { - case _: ScTypeAliasDefinition => - Some(Seq( - s"def apply(props: ${cls.name}.Props): ${cls.name}.component.Result = ???" -> Function - )) - case propsCls: ScClass if propsCls.isCase => - Some { - val paramList = propsCls.constructor.get.parameterList - val caseClassparamss = paramList.params - val childrenParam = caseClassparamss.find(_.name == "children") - - val paramssWithoutChildren = caseClassparamss.filterNot(childrenParam.contains) - - if (childrenParam.isDefined) { - if (paramssWithoutChildren.isEmpty) { - Seq( - s"def apply(${childrenParam.get.getText}): ${cls.name}.component.Result = ???" -> Function - ) + }.find(_.name == "Props") + .flatMap { elm => + elm match { + case _: ScTypeAliasDefinition => + Some( + Seq( + s"def apply(props: ${cls.name}.Props): ${cls.name}.component.Result = ???" -> Function + ) + ) + case propsCls: ScClass if propsCls.isCase => + Some { + val paramList = propsCls.constructor.get.parameterList + val caseClassparamss = paramList.params + val childrenParam = caseClassparamss.find(_.name == "children") + + val paramssWithoutChildren = caseClassparamss.filterNot(childrenParam.contains) + + if (childrenParam.isDefined) { + if (paramssWithoutChildren.isEmpty) { + Seq( + s"def apply(${childrenParam.get.getText}): ${cls.name}.component.Result = ???" -> Function + ) + } else { + val children = childrenParam.get.getType() match { + case tpe if isChildrenReactElement(tpe) => + s"${childrenParam.get.getName}: slinky.core.facade.ReactElement*" + case _ => + childrenParam.get.getText + } + + Seq( + s"def apply(${paramssWithoutChildren.map(_.getText).mkString(",")})($children): ${cls.name}.component.Result = ???" -> Function + ) + } } else { Seq( - s"def apply(${paramssWithoutChildren.map(_.getText).mkString(",")})(${childrenParam.get.getText}): ${cls.name}.component.Result = ???" -> Function + s"def apply(${paramssWithoutChildren.map(_.getText).mkString(",")}): ${cls.name}.component.Result = ???" -> Function ) } - } else { - Seq( - s"def apply(${paramssWithoutChildren.map(_.getText).mkString(",")}): ${cls.name}.component.Result = ???" -> Function - ) } - } - case _ => None + case _ => None + } } - }.getOrElse(Seq.empty) + .getOrElse(Seq.empty) applyMethods } - def isSlinky(tpe: ScTypeDefinition): Boolean = { + def isChildrenReactElement(psiType: PsiType): Boolean = + PsiTypesUtil.getPsiClass(psiType).qualifiedName match { + case "scala.collection.immutable.Seq" | "scala.collection.immutable.List" => + psiType match { + case pt: PsiClassType => + pt.getParameters.length == 1 && + pt.getParameters()(0).getCanonicalText() == "slinky.core.facade.ReactElement" + case _ => + false + } + case "slinky.core.facade.ReactElement" => + psiType.getCanonicalText.endsWith("*") + + case _ => + false + } + + def isSlinky(tpe: ScTypeDefinition): Boolean = tpe.findAnnotationNoAliases("slinky.core.annotations.react") != null - } - def isExternal(tpe: ScTypeDefinition): Boolean = { + def isExternal(tpe: ScTypeDefinition): Boolean = isSlinky(tpe) && tpe.extendsBlock.supers.map(_.getQualifiedName).exists { parent => parent == "slinky.core.ExternalComponent" || - parent == "slinky.core.ExternalComponentWithAttributes" || - parent == "slinky.core.ExternalComponentWithRefType" || - parent == "slinky.core.ExternalComponentWithAttributesWithRefType" + parent == "slinky.core.ExternalComponentWithAttributes" || + parent == "slinky.core.ExternalComponentWithRefType" || + parent == "slinky.core.ExternalComponentWithAttributesWithRefType" } - } - def isComponent(tpe: ScTypeDefinition): Boolean = { + def isComponent(tpe: ScTypeDefinition): Boolean = isSlinky(tpe) && tpe.extendsBlock.supers.map(_.getQualifiedName).exists { parent => parent == "slinky.core.Component" || parent == "slinky.core.StatelessComponent" } - } - def isFunctionalComponent(tpe: ScTypeDefinition): Boolean = { + def isFunctionalComponent(tpe: ScTypeDefinition): Boolean = isSlinky(tpe) && tpe.extendsBlock.members.exists { case td: ScValueDeclaration if td.declaredNames == Seq("component") => true case pd: ScPatternDefinition => pd.bindings.exists(_.getName == "component") case _ => false } - } - override def injectFunctions(source: ScTypeDefinition): Seq[String] = { + override def injectFunctions(source: ScTypeDefinition): Seq[String] = (source match { case obj: ScObject if isExternal(obj) => createExternalBody(obj) @@ -212,9 +259,8 @@ class SlinkyInjector extends SyntheticMembersInjector { case _ => Seq.empty }).filter(_._2 == Function).map(_._1) - } - override def injectInners(source: ScTypeDefinition): Seq[String] = { + override def injectInners(source: ScTypeDefinition): Seq[String] = (source match { case obj: ScObject => obj.fakeCompanionClassOrCompanionClass match { @@ -225,9 +271,8 @@ class SlinkyInjector extends SyntheticMembersInjector { case _ => Seq.empty }).filter(_._2 == Type).map(_._1) - } - override def injectMembers(source: ScTypeDefinition): Seq[String] = { + override def injectMembers(source: ScTypeDefinition): Seq[String] = (source match { case obj: ScObject if isExternal(obj) => createExternalBody(obj) @@ -244,9 +289,8 @@ class SlinkyInjector extends SyntheticMembersInjector { case _ => Seq.empty }).filter(_._2 == Member).map(_._1) - } - override def injectSupers(source: ScTypeDefinition): Seq[String] = { + override def injectSupers(source: ScTypeDefinition): Seq[String] = source match { case obj: ScObject => obj.fakeCompanionClassOrCompanionClass match { @@ -257,5 +301,4 @@ class SlinkyInjector extends SyntheticMembersInjector { case _ => Seq.empty } - } } diff --git a/project/plugins.sbt b/project/plugins.sbt index daae8ce6..f632b533 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,5 @@ val scalaJSVersion = - Option(System.getenv("SCALAJS_VERSION")).getOrElse("1.9.0") + Option(System.getenv("SCALAJS_VERSION")).getOrElse("1.16.0") addSbtPlugin("org.scala-js" % "sbt-scalajs" % scalaJSVersion) diff --git a/tests/src/test/scala/slinky/core/HooksComponentTest.scala b/tests/src/test/scala/slinky/core/HooksComponentTest.scala index 1e3ea2e2..4e2cd027 100644 --- a/tests/src/test/scala/slinky/core/HooksComponentTest.scala +++ b/tests/src/test/scala/slinky/core/HooksComponentTest.scala @@ -12,11 +12,11 @@ import slinky.web.ReactDOM import slinky.web.html._ import org.scalatest.Assertion -import scala.concurrent.Promise +import scala.concurrent.{ExecutionContext, Promise} import scala.util.Try class HooksComponentTest extends AsyncFunSuite { - implicit override def executionContext = scala.concurrent.ExecutionContext.Implicits.global + implicit override def executionContext: ExecutionContext = scala.concurrent.ExecutionContext.Implicits.global test("Can render a functional component with useState hook") { val container = document.createElement("div")