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
Introduction
Today, #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 #filePath
expression.
Swift-evolution thread: We need #fileName
Motivation
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
#file
string 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.
Methodology
We applied several regular expressions to all 108 projects in the Source Compatibility Suite to try to classify uses of #file
.
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 toStaticString
-taking APIs likefatalError
orXCTAssertEqual
, since there is little other reason to useStaticString
. -
281 uses matched patterns for
<StaticString typealias> = #file
where the project usually passes values of that type to APIs likefatalError
orXCTAssertEqual
. -
148 uses matched a pattern for
String = #file
, but also referenced#line
on 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#line
on the same line. We take these to be default arguments that will eventually be passed toString
-taking file APIs likeURL.init(fileURLWithPath:)
. -
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.
Proposed solution
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.[1]
The new #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 #file
today.
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 NNNN-magic-file.swift
at /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"
[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
private
andfileprivate
declarations in different files will have unique mangled names.
Detailed design
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.)
Disabling #filePath
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.
Source compatibility
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
None.
Effect on API resilience
None.
Alternatives considered
Deprecate #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:
#filePath
would continue to use the full path.#fileName
would 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:
-
The name
#fileName
is misleading, because it sounds like the string only contains the file name, but it also contains the module name.#file
is 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
#file
to one or the other option. We feel this is burdensome and unnecessary given how much more frequently the#fileName
behavior would be appropriate. -
This alternative gives users no guidance on which feature they ought to use. We feel that giving
#file
a shorter name gives users a soft push towards using it when they can, while resorting to#filePath
only when necessary.
However, it's a perfectly reasonable alternative if the Core Team thinks this proposal is too radical.
Support more than two #file
variants
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_DIR
value, 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.
Other alternatives
We considered introducing a new alternative to #file
(e.g. #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 __FILE__
anyway.)
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.