Can SourceKit say if something's a variable or a property?

I'm using SwiftSyntax to interpret a Swift file, and sometimes I need to know if an identifier is a property or a local variable. For instance:

extension String {
	var foo: String.Index {
		return startIndex // This `startIndex` here
	}
}

Can SourceKit help me find out if startIndex is a local variable I declared earlier or a property of a certain type?

I imagine there might be a few ways to do it. For instance, Xcode highlights local variables and properties in different colors, and it can also say where they were defined (e.g. inside a String class). Can I access these functionalities through SourceKit?

The response will contain key.kind which should be source.lang.swift.decl.var.local for a local variable. The list of UIDs can be found here.

Sorry, but you mean the response for which request?

Some requests have it. I think you'll want to use CursorInfo or Indexing, depending on your requirements.

Thanks, that already helps!

It would also be good to know that it's a property of String specifically, do you have any idea on how to get that type info?

Hmm I don't know, but @Nathan_Hawes might.

I don't imagine that SourceKit will be able to tell you whether you're dealing specifically with a property of Swift.String, since that's dependent on what you've imported, other types you've defined in the file, etc.

The closest you could probably get is whether it's a property of some type named "String", which maybe is good enough for your purposes?

I'm wrong, see below!

Yeah, that might be good enough!

I managed to get it (sort of) working for String (the Cursor Info for startIndex returns a "key.groupname" : "String") but it doesn't work for user-defined types, so I'd appreciate any ideas.

SourceKit knows the exact type and declaration, because it embeds the swift type checker, etc.

Here is the output of SourceKit's cursor_info on the string property:

{
  key.kind: source.lang.swift.ref.var.instance,
  key.name: "startIndex",
  key.usr: "s:SS10startIndexSS0B0Vvp",
  key.doc.full_as_xml: "<Other><Name>startIndex</Name><USR>s:SS10startIndexSS0B0Vvp</USR><Declaration>@inlinable var startIndex: String.Index { get }</Declaration><CommentParts><Abstract><Para>The position of the first character in a nonempty string.</Para></Abstract><Discussion><Para>In an empty string, <codeVoice>startIndex</codeVoice> is equal to <codeVoice>endIndex</codeVoice>.</Para></Discussion></CommentParts></Other>",
  key.typename: "String.Index",
  key.annotated_decl: "<Declaration>@inlinable @inline(__always) var startIndex: <Type usr=\"s:SS5IndexV\">Index</Type> { get }</Declaration>",
  key.fully_annotated_decl: "<decl.var.instance><syntaxtype.attribute.builtin><syntaxtype.attribute.name>@inlinable</syntaxtype.attribute.name></syntaxtype.attribute.builtin> <syntaxtype.attribute.builtin><syntaxtype.attribute.name>@inline(__always)</syntaxtype.attribute.name></syntaxtype.attribute.builtin> <syntaxtype.keyword>var</syntaxtype.keyword> <decl.name>startIndex</decl.name>: <decl.var.type><ref.struct usr=\"s:SS5IndexV\">Index</ref.struct></decl.var.type> { <syntaxtype.keyword>get</syntaxtype.keyword> }</decl.var.instance>",
  key.is_system: 1,
  key.typeusr: "$sSS5IndexVD",
  key.containertypeusr: "$sSSD",
  key.groupname: "String",
  key.overrides: [
    {
      key.usr: "s:SK10startIndex0B0Qzvp"
    },
    {
      key.usr: "s:Sl10startIndex0B0Qzvp"
    }
  ],
  key.modulename: "Swift"
}

Note the key.containertypeusr: "$sSSD",, which is Swift.String (it's supposed to be a USR, but for some reason it's using the mangling prefix "$s" instead of the USR prefix "s:" - I'm looking into whether that is a bug).

We don't currently expose the name of the containing type, but it would be reasonable to add that functionality if it's useful to you.

1 Like

Ah neat, didn't realize that! Thank you for the correction :slightly_smiling_face:

Yes, thank you, that'd be very useful! And I think I might be able to use the key.groupname workaround in the meantime.

If we're contemplating changes to SourceKit though, do you think it would be reasonable to add something like this to the Indexing (or maybe Expression Type) request instead? It just seems better for my use case than firing several Cursor Info requests, one for each identifier I come across.

I would not recommend this. The group name is not the type, it's more like a higher-level grouping of related functionality within the stdlib.

For example, Dictionary.startIndex would give you:

key.groupname: "Collection/HashedCollections"

The mapping is from stdlib source file to group and is controlled here: https://github.com/apple/swift/blob/main/stdlib/public/core/GroupInfo.json

That's too bad, I guess I'll have to prioritize changing SourceKit then. I've contributed to the Swift compiler before, but not to SourceKit specifically. Could you point me to a documentation or something that explains how to build/test/run SourceKit?

Sourcekitd is built and tested along with the compiler - the usual build-script invocations will test both. You can find the tests in test/SourceKit/*, roughly broken down by request.

Great, thanks a lot. Could you help with one more thing?

I'm thinking maybe I can do a temporary workaround using the type usr. For instance, if I knew s:SSD corresponds to String, that might solve my problem. Do you know if the usrs are the same across different Swift versions? Or, at least, different invocations of the same compiler?

The same swift toolchain will always produce the same USRs, but they're not guaranteed to be stable across different versions of swift (although in practice they don't change very often).

That's what I thought. Thank you very much :)