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/io/Serializable#,
// java/lang/CharSequence#,
// java/lang/Comparable#,
// java/lang/Object#
// )
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/Any#hashCode(). => abstract method hashCode(): Int,
// scala/Any#asInstanceOf(). => final method asInstanceOf[A](): A,
// scala/Any#`!=`(). => final method !=(that: Any): Boolean,
// scala/Any#`##`(). => final method ##(): Int,
// scala/Product#productArity(). => abstract method productArity: Int
// )
getClassMethods(Symbol("java/lang/String#")).take(5)
// res30: Set[SymbolInformation] = HashSet(
// java/lang/String#stripTrailing(). => method stripTrailing(): String,
// java/lang/String#getBytes(). => method getBytes(param0: Int, param1: Int, param2: Array[Byte], param3: Int): Unit,
// java/lang/Object#equals(). => method equals(param0: Object): Boolean,
// java/lang/Object#toString(). => method toString(): String,
// java/lang/Object#toString(). => method toString(): String
// )
getClassMethods(Symbol("scala/collection/immutable/List#")).take(5)
// res31: Set[SymbolInformation] = HashSet(
// scala/collection/IterableOps#isTraversableAgain(). => method isTraversableAgain: Boolean,
// scala/Any#getClass(). => final method getClass(): Class,
// scala/collection/IterableOps#coll(). => protected abstract method coll: C,
// scala/collection/IterableOnceOps#foldRight(). => method foldRight[B](z: B)(op: Function2[A, B, B]): B,
// scala/collection/IterableOnceOps#mkString(+1). => @inline final method mkString(sep: String): String
// )
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/CharSequence#compare(). => static method compare(param0: CharSequence, param1: CharSequence): Int,
// java/lang/String#join(). => static method join(param0: CharSequence, param1: CharSequence*): String,
// java/lang/String#lastIndexOf(+4). => private[lang] static method lastIndexOf(param0: Array[Byte], param1: Byte, param2: Int, param3: String, param4: Int): Int
// )
getClassMethods(Symbol("java/lang/String#")).filter(!_.isStatic).take(3)
// res33: Set[SymbolInformation] = HashSet(
// java/lang/Object#notifyAll(). => final method notifyAll(): Unit,
// java/lang/String#substring(+1). => method substring(param0: Int, param1: Int): String,
// java/lang/Object#finalize(). => protected method finalize(): Unit
// )
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
isSynthetic
to distinguish betweenname
andupperCaseName
.
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(+6). => method print(param0: Array[Char]): Unit,
// java/io/PrintStream#print(+2). => method print(param0: Int): Unit,
// java/io/PrintStream#print(+1). => method print(param0: Char): 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:
isLocal
isField
isMethod
isConstructor
isMacro
isType
isParameter
isSelfParameter
isTypeParameter
isObject
isPackage
isPackageObject
isClass
isInterface
isTrait
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:
isAbstract
isFinal
isSealed
isImplicit
isLazy
isCase
isCovariant
isContravariant
isVal
isVar
isStatic
isPrimary
isEnum
isDefault
isGiven
isInline
isOpen
isTransparent
isInfix
isOpaque
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:
isPrivate
isPrivateThis
isPrivateWithin
isProtected
isProtectedThis
isProtectedWithin
isPublic
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 globalvar
symbol has a corresponding setter symbol. Setter symbols are distinguished by having a display name that ends with_=
.