Find how a symbol was imported?

I have a source file which is using a particular API, without importing the package that it's defined in. As far as I can tell, in this file's imports, even transitively, that package is not imported. Yet the file compiles "correctly" at build time (though Xcode 15's live compilation agrees with me that the API should not be visible and generates live errors).

I'm using Xcode, and the file is a test, if either of those make any difference to the answer.

Is there a way I can debug how this API is being imported? Is there any mechanism other than import by which a swift API could become available to a swift source file?

2 Likes

One way is if the framework is @_exported imported elsewhere in the module.

It is @exported imported, but the target it's exported-imported from isn't (including transitively) imported...

Hmm OK, so the package structure is like,

package MyConglomeration 
    library MyToolbox
    library MyTestToolbox
        depends on MyTestUtilities
    library MyTestUtilities

Then one source file in the MyTestToolbox contains @exported import MyTestUtilities

(yes, this is pretty weird and can be flattened, but there's historical reasons it's like this)

then my code is in the tests for some other package. This package looks like

package MakeKeithQuestionLifeChoices
    library MakeKeithQuestionLifeChoices
        depends on MyToolbox
    tests MakeKeithQuestionLifeChoicesTests
        depends on MyToolbox
        depends on MyTestToolbox

Other sources in MakeKeithQuestionLifeChoicesTests do import MyTestToolbox, but this particular file doesn't, it's like:

@testable import MakeKeithQuestionLifeChoices
import MyToolbox
import XCTest

... my code, which calls an API from MyTestUtilities

Is there anything in there that seems suspicious?

I was concerned about the @_exported import so I went ahead and actually flattened the structure to remove it, and the code still compiles, so that's not relevant, and indeed the whole digression about package structure is probably irrelevant, so we can probably go back to the original question,

Is there a way I can find out, how a particular module, got (transitively) imported into a particular source file?

So I decided to do it the old fashioned way by removing imports until the code no longer compiled, and... it still compiles:

import Combine
import XCTest

final class ABCTests: XCTestCase {

    func testSomething() throws {
        //            v-- this extension method is definitely not imported in this file any more!
        _ = Just("a").record()
    }

}

Is there something special about extension methods that makes them available without imports? If I remove import Combine I can't use Just and if I remove import XCTest I can't use XCTestCase

So I split this now-empty file out into a new test target — same target dependencies, but only the one file in the target. Now it doesn't compile.

Then I added a sibling source file, containing only import MyTestToolbox. Now it does compile.

So having a sibling source file in the same module which does an import, makes all extension methods from that import, available unconditionally throughout the module?!

Excuse my incredulity, but that seems insane

TIL (negative)

2 Likes

Reproducing in isolation, brand new Package.swift:

// swift-tools-version: 5.9
import PackageDescription

let package = Package(
    name: "ExtensionMethodSiblingImport",
    targets: [
        .executableTarget(
            name: "ExtensionMethodSiblingImport",
            dependencies: [
                "StringExtension"
            ]),
        .target(name: "StringExtension"),
    ]
)

Sources/StringExtension/StringExtension.swift:

public extension String {
    func printFoo() {
        print("foo: \(self)")
    }
}

Sources/ExtensionMethodSiblingImport/main.swift:

"Hello, world!".printFoo()

swift run:

Building for debugging...
error: emit-module command failed with exit code 1 (use -v to see invocation)
/Users/bauerk1/Source/SwiftReductions/ExtensionMethodSiblingImport/Sources/ExtensionMethodSiblingImport/main.swift:4:17: error: value of type 'String' has no member 'printFoo'
"Hello, world!".printFoo()
~~~~~~~~~~~~~~~ ^~~~~~~~
/Users/bauerk1/Source/SwiftReductions/ExtensionMethodSiblingImport/Sources/ExtensionMethodSiblingImport/main.swift:4:17: error: value of type 'String' has no member 'printFoo'
"Hello, world!".printFoo()
~~~~~~~~~~~~~~~ ^~~~~~~~
error: fatalError

good :slight_smile:

Now add Sources/ExtensionMethodSiblingImport/JustImportIt.swift:

import StringExtension

swift run:

Building for debugging...
[4/4] Linking ExtensionMethodSiblingImport
Build complete! (0.85s)
foo: Hello, world!

yep :sob:

six years old, four duplicates, hard to tell with the bug tracker migration but looks like it wasn't even triaged until May this year...

:sob::sob::sob::sob::sob::sob::sob:

1 Like

I always thought this was intentional. It makes sense to me. Swift considers code (mostly) at module granularity - e.g. a global declared in one file is visible in all files in the module (with internal access level of higher). So imports are in a sense at module level too (and for practical purposes, you can't really import a library for a subset of a module).

Can you elaborate as to how / why this is a problem?

1 Like

But imports aren't "at module level"; if I want to use a type from an import, I need an import statement in that file. Same for a free function. It's only extension methods that "pollute" from sibling files. This is, at the very least, inconsistent. If we wanted "imports to be at module level", we wouldn't need import statements at all; the dependency graph from the package could import all the dependencies into all the source files. (And honestly, that'd probably be fine; it's just a different language from the one we have)

Philosophically, and rationalizing on the spot, I think maybe I prefer that "I have access to an API" means "that's my own code from this module" or "that API was explicitly imported into this source file". It feels like a good compromise between the principle of locality, and not having to constantly change use statements and access level qualifiers as I rearrange my own code, like happens in Rust, for example.

(And I think others generally agree; proposals like https://github.com/apple/swift-evolution/blob/main/proposals/0409-access-level-on-imports.md are all about reducing the "polluting" qualities of the import statement)

Technically, it is only when you want to use a type’s name that you need to import it in that file. You can use instances of that type just fine without:

// main.swift
let calendar = myCalendar()
let currentYear = calendar.component(.year, from: now())
print("It's \(currentYear)")

// This all works fine with no errors.
// The type of `calendar` is NSCalendar, but if we had written
//   let calendar: NSCalendar = myCalendar()
// that would be an error.
// file2.swift
import Foundation

func myCalendar() -> NSCalendar {
  return NSCalendar(calendarIdentifier: .gregorian)!
}

func now() -> Date {
  return Date()
}
5 Likes

That's a really good point, and casts this in a somewhat better light :slight_smile:

Though I still kinda feel, maybe that should also not work!

I suspect the ergonomics would be quite poor. If you actually think about it, you probably use a lot of things that you don't technically have direct access to (because you didn't import them in the local file).

It also would be at odds (conceptually) with how existentials work. What should your program do if some API you call returns an object of some type from a module you didn't import in your local file? Crash?!

Bottom line, there's lots of ways to access types you didn't explicitly import, and it's very convenient to not have to import them.

I like that Swift requires relatively few imports - people have wasted lifetimes on developer tooling around managing imports in languages like Python, Java, and (pre-modules) C++. :disappointed:

I think I'd prefer to not have to import anything at the file level - just define the dependencies at the module level in Package.swift - and merely have some syntax for renaming imports for the rare cases where there are conflicts. Writing import Foundation and import SwiftUI thousands of times isn't helping anything.

1 Like

I definitely don't buy this; if you've imported the protocol declaration, calling methods on objects of the existential type would be fine; if not, it would be statically disallowed, like any other type you didn't import.

This is one awkward aspect of the current system, as there's often times when you're forced to name the type explicitly (e.g. because type inference barfs) and it really shouldn't require changes like modifying imports. As noted, it makes no actual difference to how the compiler behaves (beyond type-checking performance) or how linking works.

That's one way of looking at it, but I was just observing that there's another way, which is to infer that import is about what you can access and therefore logically you should not be able to violate those rules merely because of type erasure. That would be a bug, surely?!

Things in somewhat tongue-in-cheek as I think this is taking that line of thought to its logical but absurd conclusion, but my more serious point is that the import statement really has nothing to do with what you can access or use, it really is just this kind of odd vestigial thing that mostly just controls what you're allowed to explicitly name.

1 Like

i agree completely. i’ll also say that file-level imports are bad for preventing namespace crowding - it’s way too hard to lint every file for unused imports. if they were module-scoped, it would be a lot more worthwhile to lint them aggressively and proactively.

2 Likes

I should add, though, that import can't be removed entirely, because there are perfectly valid uses of Swift which don't have any other way to import dependencies - e.g. single-file applications / scripts. I think it'd be pretty mean to require Package.swift (or equivalent Xcode project) for all Swift code that wants to import anything.

There are of course still limitations in the scenarios in question - you can only import standard system libraries, for a start, since you can't import a Git repo or similar. Though I wish, orthogonally, that you could - it'd be super handy for "shell script" usage. (it'd require a slight shift on the execution side, to essentially synthesise the equivalent Package.swift and swift run it, but it'd be quite convenient)

There was a proposal to address this, which I am having no luck searching up. I think it never went anywhere, because it tied the package manager to the language in ways people weren't ready to commit to?