SymbolInformation
Source:
SymbolInformation.scala
SymbolInformation is a data structure containing metadata about a Symbol
definition. A symbol information describes the symbols's
- display name: the identifier used to reference this symbol
- language: Scala, Java
- kind:
class,trait,object, ... - properties:
final,abstract,implicit - type signature: class declarations, class parents, method parameters, ...
- visibility access:
private,protected, ... - overridden symbols: list of symbols that this symbol overrides
Cookbook
All code examples in this document assume you have the following imports in scope
import scalafix.v1._
import scala.meta._
Get symbol of a tree
Use Tree.symbol to get the symbol of a tree. Consider the following code.
println(42)
println()
To get the println symbol we match against the Term.Name("println") tree
node.
doc.tree.collect {
case apply @ Term.Apply.After_4_6_0(println @ Term.Name("println"), _) =>
(apply.syntax, println.symbol)
}
// res1: List[(String, Symbol)] = List(
// ("println(42)", scala/Predef.println(+1).),
// ("println()", scala/Predef.println().)
// )
Lookup method return type
Use MethodSignature.returnType to inspect the return type of a method.
def printReturnType(symbol: Symbol): Unit = {
symbol.info.get.signature match {
case signature @ MethodSignature(_, _, returnType) =>
println("returnType = " + returnType)
println("signature = " + signature)
println("structure = " + returnType.structure)
}
}
printReturnType(Symbol("scala/Int#`+`()."))
// returnType = String
// signature = (x: String): String
// structure = TypeRef(NoType, Symbol("scala/Predef.String#"), List())
printReturnType(Symbol("scala/Int#`+`(+4)."))
// returnType = Int
// signature = (x: Int): Int
// structure = TypeRef(NoType, Symbol("scala/Int#"), List())
printReturnType(Symbol("scala/Option#map()."))
// returnType = Option[B]
// signature = [B](f: Function1[A,B]): Option[B]
// structure = TypeRef(
// NoType,
// Symbol("scala/Option#"),
// List(TypeRef(NoType, Symbol("scala/Option#map().[B]"), List()))
// )
The return type for constructor method signatures is always NoType.
printReturnType(Symbol("scala/Some#`<init>`()."))
// returnType = <no type>
// signature = (value: A): <no type>
// structure = NoType
Lookup method parameters
Consider the following program.
// Main.scala
package example
class Main(val constructorParam: Int) {
def magic: Int = 42
def typeParam[T]: T = ???
def annotatedParam(@deprecatedName('a) e: Int): Int = e
def curried(a: Int)(b: Int) = a + b
}
Use MethodSignature.parameterLists to look up parameters of a method.
def printMethodParameters(symbol: Symbol): Unit = {
symbol.info.get.signature match {
case signature @ MethodSignature(typeParameters, parameterLists, _) =>
println("signature = " + signature)
if (typeParameters.nonEmpty) {
println("typeParameters")
println(typeParameters.mkString(" ", "\n ", ""))
}
parameterLists.foreach { parameterList =>
println("parametersList")
println(parameterList.mkString(" ", "\n ", ""))
}
}
}
printMethodParameters(Symbol("example/Main#magic()."))
// signature = : Int
printMethodParameters(Symbol("example/Main#typeParam()."))
// signature = [T]: T
// typeParameters
// example/Main#typeParam().[T] => typeparam T
printMethodParameters(Symbol("example/Main#annotatedParam()."))
// signature = (e: Int): Int
// parametersList
// example/Main#annotatedParam().(e) => @deprecatedName param e: Int
printMethodParameters(Symbol("example/Main#`<init>`()."))
// signature = (constructorParam: Int): <no type>
// parametersList
// example/Main#`<init>`().(constructorParam) => val param constructorParam: Int
Curried methods are distinguished by a MethodSignature with a parameter list
of length greater than 1.
printMethodParameters(Symbol("example/Main#curried()."))
// signature = (a: Int)(b: Int): Int
// parametersList
// example/Main#curried().(a) => param a: Int
// parametersList
// example/Main#curried().(b) => param b: Int
printMethodParameters(Symbol("scala/Option#fold()."))
// signature = [B](ifEmpty: => B)(f: Function1[A,B]): B
// typeParameters
// scala/Option#fold().[B] => typeparam B
// parametersList
// scala/Option#fold().(ifEmpty) => param ifEmpty: => B
// parametersList
// scala/Option#fold().(f) => param f: Function1[A, B]
Test if method is nullary
A "nullary method" is a method that is declared with no parameters and without parentheses.
// Main.scala
package example
object Main {
def nullary: Int = 1
def nonNullary(): Unit = println(2)
def toString = "Main"
}
Nullary method signatures are distinguished by having an no parameter lists:
List().
def printParameterList(symbol: Symbol): Unit = {
symbol.info.get.signature match {
case MethodSignature(_, parameterLists, _) =>
println(parameterLists)
}
}
printParameterList(Symbol("example/Main.nullary()."))
// List()
printParameterList(Symbol("scala/collection/Iterator#hasNext()."))
// List()
Non-nullary methods such as Iterator.next() have a non-empty list of
parameters: List(List()).
printParameterList(Symbol("example/Main.nonNullary()."))
// List(List())
printParameterList(Symbol("scala/collection/Iterator#next()."))
// List(List())
Java does not have nullary methods so Java methods always have a non-empty list:
List(List()).
printParameterList(Symbol("java/lang/String#isEmpty()."))
// List(List())
printParameterList(Symbol("java/lang/String#toString()."))
// List(List())
Scala methods that override Java methods always have non-nullary signatures even if the Scala method is defined as nullary without parentheses.
printParameterList(Symbol("example/Main.toString()."))
// List(List())
Lookup type alias
Use TypeSignature to inspect type aliases.
def printTypeAlias(symbol: Symbol): Unit = {
symbol.info.get.signature match {
case signature @ TypeSignature(typeParameters, lowerBound, upperBound) =>
if (lowerBound == upperBound) {
println("Type alias where upperBound == lowerBound")
println("signature = '" + signature + "'")
println("typeParameters = " + typeParameters.structure)
println("bound = " + upperBound.structure)
} else {
println("Different upper and lower bounds")
println("signature = '" + signature + "'")
println("structure = " + signature.structure)
}
}
}
Consider the following program.
// Main.scala
package example
object Main {
type Number = Int
type Sequence[T] = Seq[T]
type Unbound
type LowerBound >: Int
type UpperBound <: String
type UpperAndLowerBounded >: String <: CharSequence
}
printTypeAlias(Symbol("example/Main.Number#"))
// Type alias where upperBound == lowerBound
// signature = ' = Int'
// typeParameters = List()
// bound = TypeRef(NoType, Symbol("scala/Int#"), List())
printTypeAlias(Symbol("example/Main.Sequence#"))
// Type alias where upperBound == lowerBound
// signature = '[T] = Seq[T]'
// typeParameters = List(SymbolInformation(example/Main.Sequence#[T] => typeparam T))
// bound = TypeRef(
// NoType,
// Symbol("scala/package.Seq#"),
// List(TypeRef(NoType, Symbol("example/Main.Sequence#[T]"), List()))
// )
printTypeAlias(Symbol("example/Main.Unbound#"))
// Different upper and lower bounds
// signature = ''
// structure = TypeSignature(
// List(),
// TypeRef(NoType, Symbol("scala/Nothing#"), List()),
// TypeRef(NoType, Symbol("scala/Any#"), List())
// )
printTypeAlias(Symbol("example/Main.LowerBound#"))
// Different upper and lower bounds
// signature = ' >: Int'
// structure = TypeSignature(
// List(),
// TypeRef(NoType, Symbol("scala/Int#"), List()),
// TypeRef(NoType, Symbol("scala/Any#"), List())
// )
printTypeAlias(Symbol("example/Main.UpperBound#"))
// Different upper and lower bounds
// signature = ' <: String'
// structure = TypeSignature(
// List(),
// TypeRef(NoType, Symbol("scala/Nothing#"), List()),
// TypeRef(NoType, Symbol("scala/Predef.String#"), List())
// )
printTypeAlias(Symbol("example/Main.UpperAndLowerBounded#"))
// Different upper and lower bounds
// signature = ' >: String <: CharSequence'
// structure = TypeSignature(
// List(),
// TypeRef(NoType, Symbol("scala/Predef.String#"), List()),
// TypeRef(NoType, Symbol("java/lang/CharSequence#"), List())
// )
Lookup class parents
Use ClassSignature.parents and TypeRef.symbol to lookup the class hierarchy.
def getParentSymbols(symbol: Symbol): Set[Symbol] =
symbol.info.get.signature match {
case ClassSignature(_, parents, _, _) =>
Set(symbol) ++ parents.collect {
case TypeRef(_, symbol, _) => getParentSymbols(symbol)
}.flatten
}
getParentSymbols(Symbol("java/lang/String#"))
// res28: Set[Symbol] = HashSet(
// java/lang/String#,
// java/lang/constant/ConstantDesc#,
// java/io/Serializable#,
// java/lang/CharSequence#,
// java/lang/Comparable#,
// java/lang/Object#,
// java/lang/constant/Constable#
// )
Lookup class methods
Use ClassSignature.declarations and SymbolInformation.isMethod to query
methods of a class. Use ClassSignature.parents to query methods that are
inherited from supertypes.
def getClassMethods(symbol: Symbol): Set[SymbolInformation] =
symbol.info.get.signature match {
case ClassSignature(_, parents, _, declarations) =>
val methods = declarations.filter(_.isMethod)
methods.toSet ++ parents.collect {
case TypeRef(_, symbol, _) => getClassMethods(symbol)
}.flatten
case _ => Set.empty
}
getClassMethods(Symbol("scala/Some#")).take(5)
// res29: Set[SymbolInformation] = HashSet(
// scala/Some#productPrefix(). => method productPrefix: String,
// scala/Some#equals(). => method equals(x$1: Any): Boolean,
// scala/Option#toList(). => method toList: List[A],
// scala/Option#filter(). => @inline final method filter(p: Function1[A, Boolean]): Option[A],
// scala/Some#productElementName(). => method productElementName(x$1: Int): String
// )
getClassMethods(Symbol("java/lang/String#")).take(5)
// res30: Set[SymbolInformation] = HashSet(
// java/lang/Object#getClass(). => final method getClass(): Class[local_wildcard],
// java/lang/Object#notify(). => final method notify(): Unit,
// java/lang/String#replace(). => method replace(param0: Char, param1: Char): String,
// java/lang/Object#wait(+2). => final method wait(param0: Long, param1: Int): Unit,
// java/lang/Object#hashCode(). => method hashCode(): Int
// )
getClassMethods(Symbol("scala/collection/immutable/List#")).take(5)
// res31: Set[SymbolInformation] = HashSet(
// scala/collection/IterableOnceOps#forall(). => method forall(p: Function1[A, Boolean]): Boolean,
// scala/collection/IterableOnceOps#addString(+2). => @inline final method addString(b: StringBuilder): b.type,
// scala/collection/IterableOnceOps#reduce(). => method reduce[B >: A](op: Function2[B, B, B]): B,
// scala/Any#getClass(). => final method getClass(): Class,
// scala/collection/IterableOnceOps#maxBy(). => method maxBy[B](f: Function1[A, B])(implicit ord: Ordering[B]): A
// )
For Java methods, use SymbolInformation.isStatic to separate static methods
from non-static methods.
getClassMethods(Symbol("java/lang/String#")).filter(_.isStatic).take(3)
// res32: Set[SymbolInformation] = HashSet(
// java/lang/String#join(+2). => static method join(param0: CharSequence, param1: Iterable[local_wildcard]): String,
// java/lang/String#format(). => static method format(param0: String, param1: Object*): String,
// java/lang/String#isMalformed4(). => private static method isMalformed4(param0: Int, param1: Int, param2: Int): Boolean
// )
getClassMethods(Symbol("java/lang/String#")).filter(!_.isStatic).take(3)
// res33: Set[SymbolInformation] = HashSet(
// java/lang/Object#wait(+2). => final method wait(param0: Long, param1: Int): Unit,
// java/lang/String#replaceAll(). => method replaceAll(param0: String, param1: String): String,
// java/lang/String#lines(). => method lines(): Stream[String]
// )
Lookup class primary constructor
A primary constructor is the constructor that defined alongside the class declaration.
// User.scala
package example
class User(name: String, age: Int) { // primary constructor
def this(name: String) = this(name, 42) // secondary constructor
}
Use SymbolInformation.{isConstructor,isPrimary} to distinguish a primary
constructor.
def getConstructors(symbol: Symbol): List[SymbolInformation] =
symbol.info.get.signature match {
case ClassSignature(_, _, _, declarations) =>
declarations.filter { declaration =>
declaration.isConstructor
}
case _ => Nil
}
getConstructors(Symbol("example/User#")).filter(_.isPrimary)
// res35: List[SymbolInformation] = List(
// example/User#`<init>`(). => primary ctor <init>(name: String, age: Int)
// )
// secondary constructors are distinguished by not being primary
getConstructors(Symbol("example/User#")).filter(!_.isPrimary)
// res36: List[SymbolInformation] = List(
// example/User#`<init>`(+1). => ctor <init>(name: String)
// )
Java constructors cannot be primary, "primary constructor" is a Scala-specific feature.
getConstructors(Symbol("java/lang/String#")).take(3)
// res37: List[SymbolInformation] = List(
// java/lang/String#`<init>`(). => ctor <init>(),
// java/lang/String#`<init>`(+1). => ctor <init>(param0: String),
// java/lang/String#`<init>`(+2). => ctor <init>(param0: Array[Char])
// )
getConstructors(Symbol("java/lang/String#")).filter(_.isPrimary)
// res38: List[SymbolInformation] = List()
getConstructors(Symbol("java/util/ArrayList#")).filter(_.isPrimary)
// res39: List[SymbolInformation] = List()
Lookup case class fields
Consider the following program.
// User.scala
package example
case class User(name: String, age: Int) {
def this(secondaryName: String) = this(secondaryName, 42)
val upperCaseName = name.toUpperCase
}
On the symbol information level, there is no difference between name and
upperCaseName, both are val method.
println(Symbol("example/User#name.").info)
// Some(value = example/User#name. => val method name: String)
println(Symbol("example/User#upperCaseName.").info)
// Some(
// value = example/User#upperCaseName. => val method upperCaseName: String
// )
See scalameta/scalameta#1492 for a discussion about adding
isSyntheticto distinguish betweennameandupperCaseName.
Use the primary constructor to get the names of the case class fields
getConstructors(Symbol("example/User#")).foreach {
case ctor if ctor.isPrimary =>
ctor.signature match {
case MethodSignature(_, parameters :: _, _) =>
val names = parameters.map(_.displayName)
println("names: " + names.mkString(", "))
}
case _ => // secondary constructor, ignore `this(secondaryName: String)`
}
// names: name, age
Lookup method overloads
Use SymbolInformation.{isMethod,displayName} to query for overloaded methods.
def getMethodOverloads(classSymbol: Symbol, methodName: String): Set[SymbolInformation] =
classSymbol.info.get.signature match {
case ClassSignature(_, parents, _, declarations) =>
val overloadedMethods = declarations.filter { declaration =>
declaration.isMethod &&
declaration.displayName == methodName
}
overloadedMethods.toSet ++ parents.collect {
case TypeRef(_, symbol, _) => getMethodOverloads(symbol, methodName)
}.flatten
case _ => Set.empty
}
getMethodOverloads(Symbol("java/lang/String#"), "substring")
// res44: Set[SymbolInformation] = Set(
// java/lang/String#substring(). => method substring(param0: Int): String,
// java/lang/String#substring(+1). => method substring(param0: Int, param1: Int): String
// )
getMethodOverloads(Symbol("scala/Predef."), "assert")
// res45: Set[SymbolInformation] = Set(
// scala/Predef.assert(). => @elidable method assert(assertion: Boolean): Unit,
// scala/Predef.assert(+1). => @elidable @inline final method assert(assertion: Boolean, message: => Any): Unit
// )
getMethodOverloads(Symbol("scala/Predef."), "println")
// res46: Set[SymbolInformation] = Set(
// scala/Predef.println(). => method println(): Unit,
// scala/Predef.println(+1). => method println(x: Any): Unit
// )
getMethodOverloads(Symbol("java/io/PrintStream#"), "print").take(3)
// res47: Set[SymbolInformation] = HashSet(
// java/io/PrintStream#print(+4). => method print(param0: Float): Unit,
// java/io/PrintStream#print(+6). => method print(param0: Array[Char]): Unit,
// java/io/PrintStream#print(+2). => method print(param0: Int): Unit
// )
Overloaded methods can be inherited from supertypes.
// Main.scala
package example
class Main {
def toString(width: Int): String = ???
}
getMethodOverloads(Symbol("example/Main#"), "toString")
// res49: Set[SymbolInformation] = Set(
// example/Main#toString(). => method toString(width: Int): String,
// scala/Any#toString(). => abstract method toString(): String
// )
Test if symbol is from Java or Scala
Use SymbolInformation.{isScala,isJava} to test if a symbol is defined in Java
or Scala.
def printLanguage(symbol: Symbol): Unit =
if (symbol.info.get.isJava) println("java")
else if (symbol.info.get.isScala) println("scala")
else println("unknown")
printLanguage(Symbol("java/lang/String#"))
// java
printLanguage(Symbol("scala/Predef.String#"))
// scala
Package symbols are neither defined in Scala or Java.
printLanguage(Symbol("scala/"))
// unknown
printLanguage(Symbol("java/"))
// unknown
Test if symbol is private
Access modifiers such as private and protected control the visibility of a
symbol.
def printAccess(symbol: Symbol): Unit = {
val info = symbol.info.get
println(
if (info.isPrivate) "private"
else if (info.isPrivateThis) "private[this]"
else if (info.isPrivateWithin) s"private[${info.within.get.displayName}]"
else if (info.isProtected) "protected"
else if (info.isProtectedThis) "protected[this]"
else if (info.isProtectedWithin) s"protected[${info.within.get.displayName}]"
else if (info.isPublic) "public"
else "<no access>"
)
}
Consider the following program.
// Main.scala
package example
class Main {
def publicMethod = 1
private def privateMethod = 1
private[this] def privateThisMethod = 1
private[example] def privateWithinMethod = 1
protected def protectedMethod = 1
protected[this] def protectedThisMethod = 1
protected[example] def protectedWithinMethod = 1
}
The methods have the following access modifiers.
printAccess(Symbol("example/Main#publicMethod()."))
// public
printAccess(Symbol("example/Main#privateMethod()."))
// private
printAccess(Symbol("example/Main#privateThisMethod()."))
// private[this]
printAccess(Symbol("example/Main#privateWithinMethod()."))
// private[example]
printAccess(Symbol("example/Main#protectedMethod()."))
// protected
printAccess(Symbol("example/Main#protectedThisMethod()."))
// protected[this]
printAccess(Symbol("example/Main#protectedWithinMethod()."))
// protected[example]
Observe that a symbol can only have one kind of access modifier, for example
isPrivate=false for symbols where isPrivateWithin=true.
Java does supports smaller set of access modifiers, there is no private[this],
protected[this] and protected[within] for Java symbols.
printAccess(Symbol("java/lang/String#"))
// public
println(Symbol("java/lang/String#value.").info)
// Some(
// value = java/lang/String#value. => private final field value: Array[Byte]
// )
printAccess(Symbol("java/lang/String#value."))
// private
println(Symbol("java/lang/String#`<init>`(+15).").info)
// Some(
// value = java/lang/String#`<init>`(+15). => private[lang] ctor <init>(param0: Array[Char], param1: Int, param2: Int, param3: Void)
// )
printAccess(Symbol("java/lang/String#`<init>`(+15)."))
// private[lang]
Package symbols have no access restrictions.
printAccess(Symbol("scala/"))
// <no access>
printAccess(Symbol("java/"))
// <no access>
Lookup symbol annotations
Definitions such as classes, parameters and methods can be annotated with
@annotation.
// Main.scala
package example
object Main {
@deprecated("Use add instead", "1.0")
def +(a: Int, b: Int) = add(a, b)
class typed[T] extends scala.annotation.StaticAnnotation
@typed[Int]
def add(a: Int, b: Int) = a + b
}
Use SymbolInformation.annotations to query the annotations of a symbol.
def printAnnotations(symbol: Symbol): Unit =
println(symbol.info.get.annotations.structure)
printAnnotations(Symbol("example/Main.`+`()."))
// List(Annotation(TypeRef(NoType, Symbol("scala/deprecated#"), List())))
printAnnotations(Symbol("example/Main.add()."))
// List(
// Annotation(TypeRef(
// NoType,
// Symbol("example/Main.typed#"),
// List(TypeRef(NoType, Symbol("scala/Int#"), List()))
// ))
// )
printAnnotations(Symbol("scala/Predef.identity()."))
// List(Annotation(TypeRef(NoType, Symbol("scala/inline#"), List())))
printAnnotations(Symbol("scala/Function2#[T1]"))
// List(Annotation(TypeRef(NoType, Symbol("scala/specialized#"), List())))
It is not possible to query the term arguments of annotations. For example,
observe that the annotation for Main.+ does not include the "Use add instead"
message.
Known limitations
Lookup method overrides
Consider the following program.
trait A {
def add(a: Int, b: Int): Int
}
class B extends A {
override def add(a: Int, b: Int): Int = a + b
}
There is no API to go from the symbol B#add(). to the symbol it overrides
A#add(). There is also no .isOverride helper to test if a method overrides
another symbol.
SemanticDB
The structure of SymbolInformation in Scalafix mirrors SemanticDB
SymbolInformation. For comprehensive documentation about SemanticDB symbol
information consult the SemanticDB specification:
Language
SemanticDB supports two languages: Scala and Java. Every symbol is either
defined in Scala or Java. To determine if a symbol is defined in Scala or in
Java, use the isScala and isJava methods. A symbol cannot be defined in both
Java and Scala.
Kind
Every symbol has exactly one kind such as being a class or an interface. A symbol can't have two kinds, for example it's not possible to be both a constructor and a method. The available symbol kinds are:
isLocalisFieldisMethodisConstructorisMacroisTypeisParameterisSelfParameterisTypeParameterisObjectisPackageisPackageObjectisClassisInterfaceisTrait
Some kinds are limited to specific languages. For example, Scala symbols cannot be fields and Java symbols cannot be traits.
Properties
A symbol can have zero or more properties such as implicit or final. The
available symbol properties are:
isAbstractisFinalisSealedisImplicitisLazyisCaseisCovariantisContravariantisValisVarisStaticisPrimaryisEnumisDefaultisGivenisInlineisOpenisTransparentisInfixisOpaque
Consult the SemanticDB specification to learn which properties are valid for each kind. For example, Scala traits can only be sealed, it is not valid for a trait to be implicit or final.
Signature
Signature is a sealed data structure that describes the shape of a symbol
definition.
sealed abstract class Signature extends Product with Serializable {
final override def toString: String = Pretty.pretty(this).render(80)
final def toString(width: Int): String = Pretty.pretty(this).render(width)
final def isEmpty: Boolean = this == NoSignature
final def nonEmpty: Boolean = !isEmpty
}
final case class ValueSignature(tpe: SemanticType) extends Signature
final case class ClassSignature(
typeParameters: List[SymbolInformation],
parents: List[SemanticType],
self: SemanticType,
declarations: List[SymbolInformation]
) extends Signature
final case class MethodSignature(
typeParameters: List[SymbolInformation],
parameterLists: List[List[SymbolInformation]],
returnType: SemanticType
) extends Signature
final case class TypeSignature(
typeParameters: List[SymbolInformation],
lowerBound: SemanticType,
upperBound: SemanticType
) extends Signature
case object NoSignature extends Signature
To learn more about SemanticDB signatures, consult the specification:
Annotation
To learn more about SemanticDB annotations, consult the specification:
Access
Some symbols are only accessible within restricted scopes, such as the enclosing class or enclosing package. A symbol can only have one access, for example is not valid for a symbol to be both private and private within. The available access methods are:
isPrivateisPrivateThisisPrivateWithinisProtectedisProtectedThisisProtectedWithinisPublic
To learn more about SemanticDB visibility access, consult the specification:
Utility methods
Some attributes of symbols are derived from a combination of language, kind and
property values. The following methods are available on SymbolInformation to
address common use-cases:
isDef: returns true if this symbol is a Scaladef.isSetter: returns true if this is a setter symbol. For example, every globalvarsymbol has a corresponding setter symbol. Setter symbols are distinguished by having a display name that ends with_=.
