Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix: complete backticked idents containing $ #11024

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

Merged
merged 1 commit into from
Apr 29, 2025

Conversation

harpocrates
Copy link
Contributor

The previous check around filtering out names whose decoded representation contained $ was probably written with the idea being that most dollar signs are just encoding artifacts, however it misses the fact that every once in a blue moon, the decoding result might have a $ that was actually written by the user (as opposed to something synthetic like MODULE$)

@scala-jenkins scala-jenkins added this to the 2.13.17 milestone Mar 21, 2025
@harpocrates
Copy link
Contributor Author

This works in 3.6.3.

this(sym.name) = this(sym.name) + toMember(sym, symtpe)
} else if (!sym.isError && !sym.isArtifact && sym.hasRawInfo) {
val decodedName = sym.name.decodedName
if (!decodedName.containsChar('$') || decodedName.encoded == sym.name.toString) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

!decodedName.containsChar('$') allows names that have no $ or use encodings like $lt.

decodedName.encoded == sym.name.toString allows names that have a $ but use no encoding.

So, not allowed are names that use an encoding and also have a $. Something like foo$_<. Is that the intention?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that is the intention. Are there ever any non-internal names that would use the encoding and have a $? (Is your foo$_< a legitimate user-writable thing?)

I'm a bit more concerned by the other half of the equation though: are there any internal names where the $ is escaped? I thought there wouldn't be, but there's a test failure around a $init$.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

foo$_< seems to be a legal identifier.

I think instaed of the filter that only disallows these weird identifiers, you could just as well allow any name.

The typical internal names are $outer, g$1 for a nested method g, f$default$1 for a default of method f, $init$ for trait initialization, $anon for methods implementing lambdas, foo$sp$XX for speicalized methods, and many others. Note that constructors have name $lessinit$greater / <init>.

Many of them are probably not a concern because they are created in later phases, I assume the presentation compiler queries the symbol table right after the typer phase. Some are definitely created early, but might have the isArtifact flag which is checked above. You could add another check for sym.isSynthetic, I think this would make sense, it might fix the $init$ case. (Artifact is translated to ACC_SYNTHETIC in bytecode, while synthetic remains a Scala symbol table only flag). Unfortunately the way we apply the artifact and synthetic flags is not very consistent. So if there remain synthetic identifiers that go through, we could add specific filters for those.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll try out removing any $ based checks and see how far I get with the symbol flags.

decodedName.encoded == sym.name.toString allows names that have a $ but use no encoding.

Probably moot assuming your suggestion works out, but I think I misread this. The intention is that if someone wrote $ in one of their variables it would be encoded, such that if you decode and then re-encode, you'll get the same internal (encoded) representation. OTOH, if an internal name is generated with a $ in its encoded form, then decoding passes the $ through unchanged, but re-encoding escapes it (so the quoted predicate above fails).

I agree this is pretty roundabout. Really I wanted NameTransformer.isInCodomainOfEncode and x.decoded.encoded == x is a poor-man's version of that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

if someone wrote $ in one of their variables it would be encoded

I think this is not the case, $ is not encoded as it's valid in bytecode.

scala> :power

scala> val n = TermName("$hey")

scala> n.encodedName
val res0: n.ThisNameType = $hey

scala> n.encodedName.decodedName
val res1: $r.intp.global.TermName = $hey

@harpocrates harpocrates force-pushed the completions-with-dollar-sign branch 2 times, most recently from 05c069f to d47b957 Compare April 4, 2025 00:53
@harpocrates
Copy link
Contributor Author

@lrytz I think this is ready for review again

@harpocrates harpocrates requested a review from lrytz April 8, 2025 14:46
[inaccessible] private[this] val self: callccInterpreter.type
[inaccessible] private[this] val self: callccInterpreter.type
[inaccessible] private[this] val self: callccInterpreter.type
[inaccessible] private[this] val self: callccInterpreter.type
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you know why these 4x self show up now?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, these are the fields you get from implicit classes:

  • scala.Predef.any2stringadd
  • scala.Predef.StringFormat
  • scala.Predef.Ensuring
  • scala.Predef.ArrowAssoc

In all 4 cases, the implicit class field is called self.

They did not used to show up because the decoded names have dollar signs (I'm guessing this is connected to the fact they're declare in the object Predef, though I didn't look further)

scala$Predef$any2stringadd$$self
scala$Predef$StringFormat$$self
scala$Predef$Ensuring$$self
scala$Predef$ArrowAssoc$$self

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, thanks. I don't have the background why [inaccessible] symbols are included in completions. I wonder if inaccessible symbols of implicit conversion targets should really be there as well, or if that is by mistake.

The scala$Predef$any2stringadd$$self name is an "expanded" name set by sym.makeNotPrivate, sym.hasFlag(EXPANDEDNAME) should be true here.

Copy link
Contributor Author

@harpocrates harpocrates Apr 14, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The [inaccessible] means that it doesn't actually show up in the completions. These tests have completions that generally do include results that need an implicit conversion, so I think this is the right behavior.

@@ -1009,7 +1009,7 @@ class Global(settings: Settings, _reporter: Reporter, projectName: String = "")
def add(sym: Symbol, pre: Type, implicitlyAdded: Boolean)(toMember: (Symbol, Type) => M): Unit = {
if ((sym.isGetter || sym.isSetter) && sym.accessed != NoSymbol) {
add(sym.accessed, pre, implicitlyAdded)(toMember)
} else if (!sym.name.decodedName.containsName("$") && !sym.isError && !sym.isArtifact && sym.hasRawInfo) {
} else if (!sym.name.decodedName.startsWith('$') && !sym.isError && !sym.isArtifact && sym.hasRawInfo) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you test what happens if the condition is removed entirely? Maybe we can make a more specific filter, e.g. for $init$ and ...?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing the condition causes failures only due to $init$. Full list is just

partest \
  /Users/alec/Code/scala/test/files/presentation/infix-completion \
  /Users/alec/Code/scala/test/files/presentation/infix-completion2 \
  /Users/alec/Code/scala/test/files/presentation/scope-completion-3

Only reason I didn't do that is that when I went into StdNames looking for the right constant against which to compare, I saw a bunch of other names that probably should never be in completions and which start in $. I'm not sure if those are ever going to show up...

val LAZY_SLOW_SUFFIX: NameType = nameType("$lzycompute")
val UNIVERSE_BUILD_PREFIX: NameType = nameType("$u.internal.reificationSupport.")
val UNIVERSE_PREFIX: NameType = nameType("$u.")
val UNIVERSE_SHORT: NameType = nameType("$u")
val MIRROR_PREFIX: NameType = nameType("$m.")
val MIRROR_SHORT: NameType = nameType("$m")
val MIRROR_UNTYPED: NameType = nameType("$m$untyped")
val REIFY_FREE_PREFIX: NameType = nameType("free$")
val REIFY_FREE_THIS_SUFFIX: NameType = nameType(s"$$this")
val REIFY_FREE_VALUE_SUFFIX: NameType = nameType(s"$$value") // looks like missing interpolator due to `value` in scope
val REIFY_SYMDEF_PREFIX: NameType = nameType("symdef$")
val QUASIQUOTE_CASE: NameType = nameType("$quasiquote$case$")
val QUASIQUOTE_EARLY_DEF: NameType = nameType("$quasiquote$early$def$")
val QUASIQUOTE_FILE: String = "<quasiquote>"
val QUASIQUOTE_FOR_ENUM: NameType = nameType("$quasiquote$for$enum$")
val QUASIQUOTE_NAME_PREFIX: String = "nn$"
val QUASIQUOTE_PACKAGE_STAT: NameType = nameType("$quasiquote$package$stat$")
val QUASIQUOTE_PARAM: NameType = nameType("$quasiquote$param$")
val QUASIQUOTE_PAT_DEF: NameType = nameType("$quasiquote$pat$def$")
val QUASIQUOTE_PREFIX: String = "qq$"
val QUASIQUOTE_REFINE_STAT: NameType = nameType("$quasiquote$refine$stat$")
val QUASIQUOTE_TUPLE: NameType = nameType("$quasiquote$tuple$")
val QUASIQUOTE_UNLIFT_HELPER: String = "$quasiquote$unlift$helper$"
val MIXIN_CONSTRUCTOR: NameType = nameType("$init$")
val MODULE_INSTANCE_FIELD: NameType = nameType(NameTransformer.MODULE_INSTANCE_NAME) // "MODULE$"
val OUTER: NameType = nameType("$outer")
val OUTER_LOCAL: NameType = OUTER.localName
val OUTER_ARG: NameType = nameType("arg" + OUTER)
val OUTER_SYNTH: NameType = nameType("<outer>") // emitted by pattern matcher, replaced by outer accessor in explicitouter
val ROOTPKG: NameType = nameType("_root_")
val SELECTOR_DUMMY: NameType = nameType("<unapply-selector>")
val SELF: NameType = nameType(s"$$this")
val SETTER_SUFFIX: NameType = nameType(NameTransformer.SETTER_SUFFIX_STRING)
val SPECIALIZED_INSTANCE: NameType = nameType("specInstance$")
val STAR: NameType = nameType("*")
val THIS: NameType = nameType(s"_$$this")

If you think just guarding against $init$ is enough, happy to update to that. I only left the startsWith('$') after seeing how many names in the above list start with $...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not obvious.. some of these are suffixes. $outer is not, but outer fields are probably added in a later phase. There's also the sym.isArtifact check which should be true for most synthetics, but $init$ seems to be a counter-example.

Since the goal of this PR is to show names containing $, it seems a bit ad-hoc to me to change the filter to startsWith('$'). So maybe it's better to remove it and address new unwanted candidates such as $init$.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tried what you suggested out - rather than do anything around '$', I removed the check entirely and added ARTIFACT to $init$. I did a spot check of the other $-containing symbols and AFAICT they most have ARTIFACT set or should not show up in completions for other reasons.

This does mean that more test output changes (at least those that are printing the flags for $init$ defs)

@harpocrates harpocrates force-pushed the completions-with-dollar-sign branch 3 times, most recently from 728d49a to c6db746 Compare April 23, 2025 23:11
@harpocrates
Copy link
Contributor Author

I think the CI failure is spurious...

@harpocrates harpocrates requested a review from lrytz April 25, 2025 05:06
@lrytz
Copy link
Member

lrytz commented Apr 25, 2025

@harpocrates CI is fixed.

I might have sent you down the wrong path though. The ARTIFACT flag translates into ACC_SYNTHETIC in bytecode, that change can have an effect on tools (like javac, tools working at the bytecode level). I would go for the SYNTHETIC flag instead, that may still affect Scala tools like compiler plugins, or metals, but is less risky. I think we can give this a try.

@harpocrates
Copy link
Contributor Author

Funny that ARTIFACT turns into ACC_SYNTHETIC but SYNTHETIC doesn't 😆

I can buy that ACC_SYNTHETIC might be bad for other tools operating at the byte code level, but I don't think SYNTHETIC will help for my case here - the completions filter is only on isArtifact and using isSynthetic will filter out things the user will care about (I forget all of them, but aren't apply/unapply in a case class for example synthetic?)

What do you propose there? Going back to just checking against the $init$ name itself?

@lrytz
Copy link
Member

lrytz commented Apr 28, 2025

Funny that ARTIFACT turns into ACC_SYNTHETIC but SYNTHETIC doesn't 😆

Yeah, it's history and probably not worth changing at this point..

using isSynthetic will filter out things the user will care about

Ah.. you're right

scala> :power
scala> case class C(x: Int)
scala> typeOf[C].members.filter(_.isSynthetic)
val res0: $r.intp.global.Scope =
Scope{
  override def equals(x$1: Any): Boolean;
  override def toString(): String;
  override def hashCode(): Int;
  override def productElementName(x$1: Int): String;
  def canEqual(x$1: Any): Boolean;
  override def productIterator: Iterator[Any];
  def productElement(x$1: Int): Any;
  def productArity: Int;
  override def productPrefix: String;
  def copy$default$1: Int @scala.annotation.unchecked.uncheckedVariance;
  def copy(x: Int): C
}

None of these have the isArtifact flag.

So yes, I think we should go for filtering $init$ by name. How about the default getters? For them you could use the sym.isDefaultGetter.

@harpocrates
Copy link
Contributor Author

Sounds good - doing this now.

@lrytz Unrelated, but now that 2.13.17 is in snapshot mode, I get errors trying to compile anything in SBT:

...
[info] resolving key references (28404 settings) ...
[info] *** Welcome to the sbt build definition for Scala! ***
[info] version=2.13.17-bin-SNAPSHOT scalaVersion=2.13.17-M1
[info] Check README.md for more information.
[info] Updating scala-library
[info] Resolved scala-library dependencies
[warn] 
[warn]  Note: Unresolved dependencies path:
[error] sbt.librarymanagement.ResolveException: Error downloading org.scalameta:semanticdb-scalac_2.13.17-M1:4.13.4
[error]   Not found
[error]   Not found
[error]   not found: /Users/alec/.ivy2/local/org.scalameta/semanticdb-scalac_2.13.17-M1/4.13.4/ivys/ivy.xml
[error]   not found: https://repo1.maven.org/maven2/org/scalameta/semanticdb-scalac_2.13.17-M1/4.13.4/semanticdb-scalac_2.13.17-M1-4.13.4.pom
[error]         at lmcoursier.CoursierDependencyResolution.unresolvedWarningOrThrow(CoursierDependencyResolution.scala:347)
[error]         at lmcoursier.CoursierDependencyResolution.$anonfun$update$39(CoursierDependencyResolution.scala:316)
[error]         at scala.util.Either$LeftProjection.map(Either.scala:573)
[error]         at lmcoursier.CoursierDependencyResolution.update(CoursierDependencyResolution.scala:316)
[error]         at sbt.librarymanagement.DependencyResolution.update(DependencyResolution.scala:60)
[error]         at sbt.internal.LibraryManagement$.resolve$1(LibraryManagement.scala:60)

I can work around this by rebasing onto an old master, but Is there anything to better do about this?

@lrytz
Copy link
Member

lrytz commented Apr 28, 2025

I assume you opened the repo in vs code / metals, which added the semanticdb plugin.

IIRC metals adds a metals.sbt file or similar, right? Can you force the semanticdb dependency to use the semanticdb-scalac_2.13.16 version? We usually do that when troubleshooting a non-released scala version on a community project that depends on a compiler plugin.

Or you revert starr.version=2.13.16 in versions.properties locally.

Replace the ad-hoc check for hiding from completions identifiers
containing a `$` with a check against specific flavours of
compiler-generated $-containing names.
@harpocrates harpocrates force-pushed the completions-with-dollar-sign branch from c6db746 to 1b0c18f Compare April 29, 2025 05:49
Copy link
Member

@lrytz lrytz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you! Sorry it was a winding road.

@lrytz lrytz merged commit e19b69f into scala:2.13.x Apr 29, 2025
3 checks passed
@harpocrates harpocrates deleted the completions-with-dollar-sign branch April 29, 2025 08:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants