Repository: scalameta/scalameta (scala)

Issue:
scalafmt replaces `Resource[F,_]` with `Resource[F,?]` which does not compile
This template is mostly a guideline, not a strict requirement, except
the sections marked `(required)`.

Please make sure that the issue is observable using the latest version of `scalafmt` and
within the `scalafmt` **CLI**: `https://scalameta.org/scalafmt/docs/installation.html#cli`

Using the CLI is _mandatory_, in one of these configurations:
- latest version of CLI and latest version in `.scalafmt.conf`
- previous version of CLI and latest version in `.scalafmt.conf`
- latest version of CLI and previous version in `.scalafmt.conf`

## Configuration (required) ##

Please paste the contents of your `.scalafmt.conf` file here:
```
version = "3.7.4"
runner.dialect = scala3
indent {
  main = 2
  matchSite = 1
  significant = 3
}
align {
  preset = none
  stripMargin = false
}
maxColumn = 100
assumeStandardLibraryStripMargin = true
rewrite.scala3 {
  convertToNewSyntax = true
  removeOptionalBraces = yes
}
# runner.dialectOverride.allowQuestionMarkAsTypeWildcard = false
newlines {
  selectChains = fold
  beforeMultiline = fold
}
comments.wrapSingleLineMlcAsSlc = false
docstrings{
  wrap = "no"
  oneline = fold
  style = SpaceAsterisk 
}


fileOverride {
  "glob:**.sbt" {
    runner.dialect = scala212source3
  }

  "glob:**/project/**.scala" {
    runner.dialect = scala212source3
  }
  "glob:**/interface/**.scala" {
    runner.dialect = scala212source3
  }

  "glob:**/sbt-plugin/**.scala" {
    runner.dialect = scala212source3
  }
}
```

## Command-line parameters (required) ##

When I run scalafmt via CLI like this: 
`scala-cli format` or from Visual Studio Code or `scalafmt wallet` from the root of the git directory for
[SolidCtrolApp CommandLine branch](https://github.com/bblfish/SolidCtrlApp/tree/CommandLine) with commit bde97487078875f75414740a336654d5fc805913 and after  commenting out the following line from the scalafmt.conf file which resolves the problem.

```
# runner.dialectOverride.allowQuestionMarkAsTypeWildcard = false
```

## Steps

Given code like this [wallet/shared/src/main/scala/net/bblfish/wallet/BasicAuthWallet.scala#L402](https://github.com/bblfish/SolidCtrlApp/blob/bde97487078875f75414740a336654d5fc805913/wallet/shared/src/main/scala/net/bblfish/wallet/BasicAuthWallet.scala#L402)

```scala
  def findCachedAclFor(
       requestUrl: ll.AbsoluteUrl,
       depth: Int = 50
   ): Resource[F, RDF.Graph[Rdf]] =
      val onlyCache: Boolean = true
      // we start by making a HEAD request, because we just want to find the link to the acl,
      // if requestUrl is the acl, then it may link back to itself, giving us what we want, or it
      // will link to nothing, in which case we won't notice which is a problem, in which case we won't notice which is a problem
      // todo: have acls link back to themselves on the server as per https://github.com/solid/authorization-panel/issues/189
      //   or find another way to allow the client to tell it is on an acl.
      fetchRdf(requestUrl, HEAD, onlyCache).flatMap { crG =>
         val links = extractLinks(requestUrl, crG.headers)
         // we signal an continuable failure by plaing it in the internal Option
         val defaultOrRel: Resource[F, Option[RDF.Graph[Rdf]]] =
           if onlyCache && crG.status == h4s.Status.GatewayTimeout
           then // we retrun a resource with a None, meaning we can continue looking in the cache
              Resource.liftK(fc.pure[Option[RDF.Graph[Rdf]]](None))
           else
              for
                 optDeflt <- links.collectFirst { case (Right(`defaultAC`), uri) =>
                   findAclForContainer(uri, onlyCache)
                 }.sequence
                 // if the defaultAC link above fails we fail otherwise
                 optEAcl <- optDeflt match
                  case Some(_) => optDeflt.map(Right[Throwable, RDF.Graph[Rdf]])
                      .pure[Resource[F, _]]
                  case None => links.collectFirst { case (Right("acl"), uri) =>
                      fetchAclGr(uri, onlyCache)
                    }.map(_.attempt).sequence
              yield optEAcl.flatMap(_.toOption)

         // fetching acl directly can fail we should continue if it does
         defaultOrRel.flatMap {
           case Some(gr) => gr.pure[Resource[F, _]]
           case None => findContainerFor(requestUrl, links, onlyCache, depth) match
              case None => Resource.raiseError[F, RDF.Graph[Rdf], Throwable](
                  Exception(s"No useable link to find ACL from $requestUrl")
                )
              case Some(url) => findCachedAclFor(url, depth - 1)
         }
      }
   end findCachedAclFor
```

## Problem

Scalafmt formats code like this notice the use if `?` in `pure[Resource[F,?]` rather than `pure[Resource[F,_]]`

```scala
   def findCachedAclFor(
       requestUrl: ll.AbsoluteUrl,
       depth: Int = 50
   ): Resource[F, RDF.Graph[Rdf]] =
      val onlyCache: Boolean = true
      // we start by making a HEAD request, because we just want to find the link to the acl,
      // if requestUrl is the acl, then it may link back to itself, giving us what we want, or it
      // will link to nothing, in which case we won't notice which is a problem, in which case we won't notice which is a problem
      // todo: have acls link back to themselves on the server as per https://github.com/solid/authorization-panel/issues/189
      //   or find another way to allow the client to tell it is on an acl.
      fetchRdf(requestUrl, HEAD, onlyCache).flatMap { crG =>
         val links = extractLinks(requestUrl, crG.headers)
         // we signal an continuable failure by plaing it in the internal Option
         val defaultOrRel: Resource[F, Option[RDF.Graph[Rdf]]] =
           if onlyCache && crG.status == h4s.Status.GatewayTimeout
           then // we retrun a resource with a None, meaning we can continue looking in the cache
              Resource.liftK(fc.pure[Option[RDF.Graph[Rdf]]](None))
           else
              for
                 optDeflt <- links.collectFirst { case (Right(`defaultAC`), uri) =>
                   findAclForContainer(uri, onlyCache)
                 }.sequence
                 // if the defaultAC link above fails we fail otherwise
                 optEAcl <- optDeflt match
                  case Some(_) => optDeflt.map(Right[Throwable, RDF.Graph[Rdf]])
                      .pure[Resource[F, ?]]
                  case None => links.collectFirst { case (Right("acl"), uri) =>
                      fetchAclGr(uri, onlyCache)
                    }.map(_.attempt).sequence
              yield optEAcl.flatMap(_.toOption)

         // fetching acl directly can fail we should continue if it does
         defaultOrRel.flatMap {
           case Some(gr) => gr.pure[Resource[F, ?]]
           case None => findContainerFor(requestUrl, links, onlyCache, depth) match
              case None => Resource.raiseError[F, RDF.Graph[Rdf], Throwable](
                  Exception(s"No useable link to find ACL from $requestUrl")
                )
              case Some(url) => findCachedAclFor(url, depth - 1)
         }
      }
   end findCachedAclFor
```

## Expectation

The original output.

## Workaround

Found on [typelevel discord by Daenyth](https://discord.com/channels/632277896739946517/632277897448652844/1114251656515817542)
add to  .scalafmt.conf
```
runner.dialectOverride.allowQuestionMarkAsTypeWildcard = false
```

## Notes

See also https://discord.com/channels/632642981228314653/632665341864181780/1115151820352934008
It is odd that in such a large code base the only place that change is made is in that particular file.

Pull request description:
Fixes #3162.

Patch:
diff --git a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/NestedContext.scala b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/NestedContext.scala
index 0e490ea9f2..0812087770 100644
--- a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/NestedContext.scala
+++ b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/NestedContext.scala
@@ -11,7 +11,8 @@ private[parsers] trait NestedContext {
       nested -= 1
     }
   }
-  def isInside() = nested > 0
+  @inline def isInside() = isDeeper(0)
+  @inline def isDeeper(level: Int) = nested > level
 }
 
 private[parsers] object QuotedSpliceContext extends NestedContext
@@ -19,3 +20,5 @@ private[parsers] object QuotedSpliceContext extends NestedContext
 private[parsers] object QuotedPatternContext extends NestedContext
 
 private[parsers] object ReturnTypeContext extends NestedContext
+
+private[parsers] object TypeBracketsContext extends NestedContext
diff --git a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScalametaParser.scala b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScalametaParser.scala
index 513df3dd4d..e63dd48b17 100644
--- a/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScalametaParser.scala
+++ b/scalameta/parsers/shared/src/main/scala/scala/meta/internal/parsers/ScalametaParser.scala
@@ -809,7 +809,8 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
 
     def entrypointType(): Type = paramType()
 
-    def typeArgs(): Type.ArgClause = autoPos(inBrackets(types()).reduceWith(Type.ArgClause.apply))
+    def typeArgs(): Type.ArgClause =
+      TypeBracketsContext.within(autoPos(inBrackets(types()).reduceWith(Type.ArgClause.apply)))
 
     def infixTypeOrTuple(inMatchType: Boolean = false): Type = {
       if (token.is[LeftParen]) tupleInfixType(allowFunctionType = !inMatchType)
@@ -959,7 +960,10 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
           Type.Macro(macroSplice())
         case _: Underscore if inMatchType =>
           next(); Type.PatWildcard()
-        case _: Underscore if dialect.allowUnderscoreAsTypePlaceholder =>
+        case _: Underscore
+            if dialect.allowUnderscoreAsTypePlaceholder ||
+              dialect.allowTypeLambdas &&
+              TypeBracketsContext.isDeeper(1) && !peekToken.isAny[Supertype, Subtype] =>
           next(); Type.AnonymousParam(None)
         case _: Underscore =>
           wildcardType()
@@ -3202,9 +3206,9 @@ class ScalametaParser(input: Input)(implicit dialect: Dialect) { parser =>
       ctxBoundsAllowed: Boolean,
       allowUnderscore: Boolean = true
   ): Type.ParamClause = {
-    autoPos(inBrackets(commaSeparated {
+    TypeBracketsContext.within(autoPos(inBrackets(commaSeparated {
       typeParam(ownerIsType, ctxBoundsAllowed, allowUnderscore)
-    }.reduceWith(Type.ParamClause.apply)))
+    }.reduceWith(Type.ParamClause.apply))))
   }
 
   def typeParam(


Test patch:
diff --git a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/TypeSuite.scala b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/TypeSuite.scala
index 37a1c6fdd0..42948eae16 100644
--- a/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/TypeSuite.scala
+++ b/tests/shared/src/test/scala/scala/meta/tests/parsers/dotty/TypeSuite.scala
@@ -527,4 +527,94 @@ class TypeSuite extends BaseDottySuite {
     assertNoDiff(err.shortMessage, "can't have multiple parameter lists in function types")
   }
 
+  test("#3162 [scala30] higher-kinded is not wildcard 1") {
+    implicit val dialect: Dialect = dialects.Scala30
+    runTestAssert[Stat]("def foo[A <: C[_]] = bar.baz[_, F[_]]")(
+      Defn.Def(
+        Nil,
+        tname("foo"),
+        Type.Param(
+          Nil,
+          pname("A"),
+          Nil,
+          Type.Bounds(
+            None,
+            Some(Type.AnonymousLambda(Type.Apply(pname("C"), List(Type.AnonymousParam(None)))))
+          ),
+          Nil,
+          Nil
+        ) :: Nil,
+        Nil,
+        None,
+        Term.ApplyType(
+          Term.Select(tname("bar"), tname("baz")),
+          List(
+            Type.Wildcard(noBounds),
+            Type.AnonymousLambda(Type.Apply(pname("F"), List(Type.AnonymousParam(None))))
+          )
+        )
+      )
+    )
+  }
+
+  test("#3162 [scala3+] higher-kinded is not wildcard 1") {
+    implicit val dialect: Dialect = dialects.Scala3.withAllowUnderscoreAsTypePlaceholder(true)
+    runTestAssert[Stat]("def foo[A <: C[_]] = bar.baz[_, F[_]]")(
+      Defn.Def(
+        Nil,
+        tname("foo"),
+        Type.Param(
+          Nil,
+          pname("A"),
+          Nil,
+          Type.Bounds(
+            None,
+            Some(Type.AnonymousLambda(Type.Apply(pname("C"), List(Type.AnonymousParam(None)))))
+          ),
+          Nil,
+          Nil
+        ) :: Nil,
+        Nil,
+        None,
+        Term.ApplyType(
+          Term.Select(tname("bar"), tname("baz")),
+          List(
+            Type.AnonymousParam(None),
+            Type.AnonymousLambda(Type.Apply(pname("F"), List(Type.AnonymousParam(None))))
+          )
+        )
+      )
+    )
+  }
+
+  test("#3162 [scala30] higher-kinded is not wildcard 2") {
+    implicit val dialect: Dialect = dialects.Scala30
+    runTestAssert[Stat]("gr.pure[Resource[F, _]]")(
+      Term.ApplyType(
+        Term.Select(tname("gr"), tname("pure")),
+        Type.AnonymousLambda(
+          Type.Apply(
+            pname("Resource"),
+            List(pname("F"), Type.AnonymousParam(None))
+          )
+        ) :: Nil
+      )
+    )
+  }
+
+  test("#3162 [scala3+] higher-kinded is not wildcard 2") {
+    implicit val dialect: Dialect = dialects.Scala3.withAllowUnderscoreAsTypePlaceholder(true)
+    runTestAssert[Stat]("gr.pure[Resource[F, _]]")(
+      Term.ApplyType(
+        Term.Select(tname("gr"), tname("pure")),
+        Type.AnonymousLambda(
+          Type.Apply(
+            pname("Resource"),
+            List(pname("F"), Type.AnonymousParam(None))
+          )
+        ) :: Nil
+      )
+    )
+  }
+
 }