In previous threads like We Need #fileName, we've generally agreed that the current
#file feature makes some of the wrong tradeoffs by generating such a verbose path. I've prepared a proposal (and a matching implementation) to fix this problem.
Thanks to @davedelong for getting this process started.
Concise magic file names
- Proposal: SE-NNNN
- Authors: Brent Royal-Gordon, Dave DeLong
- Review Manager: TBD
- Status: Awaiting review
- Implementation: apple/swift#25656
#file evaluates to a string literal containing the full path to the current source file. We propose to instead have it evaluate to a human-readable string containing the filename and module name, while preserving the existing behavior in a new
Swift-evolution thread: We need
In Swift today, the magic identifier
#file evaluates to a string literal containing the full path to the current file. It's a nice way to trace the location of logic occurring in a Swift process, but its use of a full path has a lot of drawbacks:
It clutters the debug output with irrelevant information. The path is usually very long and only a little bit of that information is necessary to locate the file in question. In a hundred-character path, the developer usually only cares about the last ten or twenty.
It's not portable. The same project may be located at different paths on different machines; a developer looking at a crash log doesn't care about a path on a build server.
It can inadvently reveal private or sensitive information. The full path to a source file may contain a developer's username, hints about the configuration of a build farm, proprietary versions or identifiers, or the Sailor Scout you named an external disk after. Users probably don't know that this information is embedded in their binaries and may not want it to be there.
It bloats the final size of the binary. In testing with the Swift benchmark suite, a shorter
#filestring reduced code size by up to 5%. The large code also impacts runtime performance; in the same tests, a couple dozen benchmarks ran noticeably faster, with several taking 22% less time.
It introduces artificial differences between binaries built on different machines. For instance, the same code built in two different environments might produce different binaries with different hashes. This makes life difficult for anyone trying to do distributed builds or find the differences between two binaries.
Situations where the full path is needed
While the full path is not needed when printing messages for the developer, some uses of
#file do rely on it. In particular, Swift tests sometimes use
#file to compute paths to fixtures relative to the source file that uses them. This has historically been necessary in SwiftPM because it did not support resources, but SE-0271 has added that feature and there is little need to resort to these tricks anymore.
An analysis of the 1,073 places where
#file is written in the Swift Source Compatibility Suite suggests that well over 90% of uses would be better served by a
#file that did not include a full path. However, we do need to make some concession to the small portion of uses that need a full path for some reason.
We applied several regular expressions to all 108 projects in the Source Compatibility Suite to try to classify uses of
980 uses matched patterns that we believe represent display to humans:
419 uses matched a pattern for
StaticString = #file; we take these to be default arguments that are eventually passed to
StaticString-taking APIs like
XCTAssertEqual, since there is little other reason to use
281 uses matched patterns for
<StaticString typealias> = #filewhere the project usually passes values of that type to APIs like
148 uses matched a pattern for
String = #file, but also referenced
#lineon the same line. We take these to be attempts to capture a full source location for display to the user.
132 uses matched a pattern for interpolations of
#file; we take these to be interpolated into a string that is then displayed to a user.
41 uses matched patterns that we believe represent path computation:
10 uses matched a pattern for
String = #file, but did not have
#lineon the same line. We take these to be default arguments that will eventually be passed to
String-taking file APIs like
31 uses matched a pattern for uses in parenthesized lists (but didn't match the interpolation pattern); we take these to be passed to file APIs.
52 uses did not match any of these patterns.
We therefore estimate that about 6% (±3%) of uses actually want a full path so they can compute paths to other files, while 94% (±3%) would be better served by a more succinct string.
A manual check of 172 uses in 16 projects suggested that about 95% displayed the
#file value to the user; this is in line with the regex-based estimate.
We propose that
#file should evaluate to a human-readable string which uniquely identifies a source file in the process, but which does not contain the full path. Specifically, it will contain the file name and module name.
#file will otherwise behave as it did before, including its special behavior in default arguments. Standard library assertion functions will continue to use
#file, and we encourage developers to use it in test helpers and most other places where they use
For those rare cases where developers actually need a full path, we propose adding a
#filePath magic identifier with the same behavior that
#file had in previous versions of Swift.
In a module named
MagicFile and a file named
/Users/brent/Desktop, these features might result in output like this:
print(#file) // => "NNNN-magic-file.swift (MagicFile)" print(#filePath) // => "/Users/brent/Desktop/NNNN-magic-file.swift" fatalError("Something bad happened!") // => "Fatal error: Something bad happened!: file NNNN-magic-file.swift (MagicFile), line 1"
 This is sufficient to uniquely identify a file because the Swift compiler will not build a module which contains two identically-named source files, even if they're in different directories. This limitation ensures that identically-named
fileprivatedeclarations in different files will have unique mangled names.
We do not specify the exact string used in
#file—we only specify that it is human-readable and most likely unique in the process. (We expect some of the notation for module names to change, and we'd like to be able to make the string match it without another proposal.)
Although it is not technically part of this proposal, we are considering adding a new compiler flag which distributed build systems can use to disable
#filePath and other features incompatible with their build model.
All existing source code will continue to compile, but the compiler will generate different strings for
#file expressions. We anticipate that this will change the behavior of a small amount of existing code in non-trivial ways. However, we believe that this will most heavily impact tests and test support libraries, resulting in easily detected test failures rather than hidden bugs, and that adding
#filePath makes these failures easy to correct.
Effect on ABI stability
Effect on API resilience
#file and introduce two new syntaxes
Rather than changing the meaning of
#file, we could keep its existing behavior, deprecate it, and provide two alternatives:
#filePathwould continue to use the full path.
#fileNamewould use this new name-and-module string.
This is a more conservative approach that would avoid breaking any existing uses. We choose not to propose it for three reasons:
#fileNameis misleading, because it sounds like the string only contains the file name, but it also contains the module name.
#fileis more vague, so we're more comfortable saying that it's "a string that identifies the file".
This alternative will force users to update every use of
#fileto one or the other option. We feel this is burdensome and unnecessary given how much more frequently the
#fileNamebehavior would be appropriate.
This alternative gives users no guidance on which feature they ought to use. We feel that giving
#filea shorter name gives users a soft push towards using it when they can, while resorting to
#filePathonly when necessary.
However, it's a perfectly reasonable alternative if the Core Team thinks this proposal is too radical.
Support more than two
We considered introducing additional
#file-like features to generate other strings, selecting between them either with a compiler flag or with different magic identifiers. The full set of behaviors we considered included:
- Path as written in the compiler invocation
- Guaranteed-absolute path
- Path relative to the Xcode
SOURCE_DIRvalue, or some equivalent
- Last component of the path (file name only)
- File name plus module name
- Empty string (sensible as a compiler flag)
We ultimately decided that supporting only 1 (as
#filePath) and 5 (as
#file) would adequately cover the use cases for
#file. Five different syntaxes would devote a lot of language surface area to a small niche, and controlling the behavior with a compiler flag would create six language dialects that might break some code. Some of these behaviors would also require introducing new concepts into the compiler or would cause trouble for distributed build systems.
One particularly interesting change from our approach would be to replace
#filePath's behavior 1 (path as written in the compiler invocation) with behavior 2 (guaranteed-absolute path); after all, a guaranteed-absolute path may be easier to process than one that depends on the compiler invocation. It seems like a reasonable alternative, but we think we could make that change later if we wished.
We considered introducing a new alternative to
#fileName) while preserving the existing meaning of
#file. However, a great deal of code already uses
#file and would in practice probably never be converted to
#fileName. The vast majority of this code would benefit from the new behavior, so we think it would be better to automatically adopt it. (Note that clang supports a
__FILE_NAME__ alternative, but most code still uses
We considered switching between the old and new
#file behavior with a compiler flag. However, this creates a language dialect, and compiler flags are not a natural interface for users.
Finally, we could change the behavior of
#file without offering an escape hatch. However, we think that the existing behavior is useful in rare circumstances and should not be totally removed.