-
Notifications
You must be signed in to change notification settings - Fork 1.4k
add-target
breaks resulting package when target name is not valid Swift identifier
#7764
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
9e15797
e515806
627b625
88ffe20
3160ed5
d8d4e3f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -234,10 +234,10 @@ public struct AddTarget { | |
case .macro: | ||
""" | ||
\(imports) | ||
struct \(raw: target.name): Macro { | ||
struct \(raw: target.sanitizedName): Macro { | ||
/// TODO: Implement one or more of the protocols that inherit | ||
/// from Macro. The appropriate macro protocol is determined | ||
/// by the "macro" declaration that \(raw: target.name) implements. | ||
/// by the "macro" declaration that \(raw: target.sanitizedName) implements. | ||
/// Examples include: | ||
/// @freestanding(expression) macro --> ExpressionMacro | ||
/// @attached(member) macro --> MemberMacro | ||
|
@@ -255,8 +255,8 @@ public struct AddTarget { | |
case .xctest: | ||
""" | ||
\(imports) | ||
class \(raw: target.name): XCTestCase { | ||
func test\(raw: target.name)() { | ||
class \(raw: target.sanitizedName)Tests: XCTestCase { | ||
func test\(raw: target.sanitizedName)() { | ||
XCTAssertEqual(42, 17 + 25) | ||
} | ||
} | ||
|
@@ -266,8 +266,8 @@ public struct AddTarget { | |
""" | ||
\(imports) | ||
@Suite | ||
struct \(raw: target.name)Tests { | ||
@Test("\(raw: target.name) tests") | ||
struct \(raw: target.sanitizedName)Tests { | ||
@Test("\(raw: target.sanitizedName) tests") | ||
func example() { | ||
#expect(42 == 17 + 25) | ||
} | ||
|
@@ -284,7 +284,7 @@ public struct AddTarget { | |
""" | ||
\(imports) | ||
@main | ||
struct \(raw: target.name)Main { | ||
struct \(raw: target.sanitizedName)Main { | ||
static func main() { | ||
print("Hello, world") | ||
} | ||
|
@@ -313,9 +313,9 @@ public struct AddTarget { | |
import SwiftCompilerPlugin | ||
|
||
@main | ||
struct \(raw: target.name)Macros: CompilerPlugin { | ||
struct \(raw: target.sanitizedName)Macros: CompilerPlugin { | ||
let providingMacros: [Macro.Type] = [ | ||
\(raw: target.name).self, | ||
\(raw: target.sanitizedName).self, | ||
] | ||
} | ||
""" | ||
|
@@ -414,3 +414,15 @@ fileprivate extension PackageDependency { | |
) | ||
} | ||
} | ||
|
||
fileprivate extension TargetDescription { | ||
var sanitizedName: String { | ||
name | ||
.spm_mangledToC99ExtendedIdentifier() | ||
.capitalizingFirstLetter() | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using the Keyword enum turned out to be not that trivial for this task, as there are many exceptions due to the keyword's context-specific nature, namely:
As simple solutions are generally better, I preferred to use
I believe the combination of these two safeguards covers all known edge cases (to the best of my knowledge). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah sorry, I missed the discussion above. We actually added There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, thanks! That's really nice to know. |
||
} | ||
|
||
fileprivate extension String { | ||
func capitalizingFirstLetter() -> String { prefix(1).uppercased() + dropFirst() } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using both There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've decided to introduce On the other hand, capitalizing only the first letter would turn "my-new-shiny-target" into "My_new_shiny_target" instead of the potentially preferred "My_New_Shiny_Target" (which There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, worth taking a moment to write some unit tests where the macro name does not use the English Latin alphabet. For instance, does the French "être" correctly capitalize to "Être"? Does the Japanese "マクロ" stay unmodified? These are trivial test cases but worth adding. More complex to solve would be something like the "Turkish I" capitalization problem (uppercase "i" in Turkish is "İ", not "I", and lowercase "I" is "ı".) Should this code attempt to take the user's localization settings into consideration? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, that's a good point again. I switched to I'd love to write more tests that cover localization edge cases. However, this would require some refactoring of the test helpers (as I mentioned here) if we want to avoid significant duplication in tests. At the same time, what we have might already be good enough for this specific use case, as the main goal of this PR was to prevent broken code in modified packages in the first place. @DougGregor I'd also like to hear your opinion. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Honestly I'm not sure There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just checked, and
At the same time, the system
Meaning, |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -468,7 +468,7 @@ class ManifestEditTests: XCTestCase { | |
// These are the targets | ||
.target(name: "MyLib"), | ||
.executableTarget( | ||
name: "MyProgram", | ||
name: "MyProgram target-name", | ||
dependencies: [ | ||
.product(name: "SwiftSyntax", package: "swift-syntax"), | ||
.target(name: "TargetLib"), | ||
|
@@ -479,13 +479,13 @@ class ManifestEditTests: XCTestCase { | |
) | ||
""", | ||
expectedAuxiliarySources: [ | ||
RelativePath("Sources/MyProgram/MyProgram.swift") : """ | ||
RelativePath("Sources/MyProgram target-name/MyProgram target-name.swift") : """ | ||
import MyLib | ||
import SwiftSyntax | ||
import TargetLib | ||
|
||
@main | ||
struct MyProgramMain { | ||
struct MyProgram_target_nameMain { | ||
static func main() { | ||
print("Hello, world") | ||
} | ||
|
@@ -494,7 +494,7 @@ class ManifestEditTests: XCTestCase { | |
]) { manifest in | ||
try AddTarget.addTarget( | ||
TargetDescription( | ||
name: "MyProgram", | ||
name: "MyProgram target-name", | ||
dependencies: [ | ||
.product(name: "SwiftSyntax", package: "swift-syntax"), | ||
.target(name: "TargetLib", condition: nil), | ||
|
@@ -528,7 +528,7 @@ class ManifestEditTests: XCTestCase { | |
], | ||
targets: [ | ||
.macro( | ||
name: "MyMacro", | ||
name: "MyMacro target-name", | ||
dependencies: [ | ||
.product(name: "SwiftCompilerPlugin", package: "swift-syntax"), | ||
.product(name: "SwiftSyntaxMacros", package: "swift-syntax") | ||
|
@@ -538,33 +538,33 @@ class ManifestEditTests: XCTestCase { | |
) | ||
""", | ||
expectedAuxiliarySources: [ | ||
RelativePath("Sources/MyMacro/MyMacro.swift") : """ | ||
RelativePath("Sources/MyMacro target-name/MyMacro target-name.swift") : """ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Though spaces work for file names, we could consider sanitizing them too. |
||
import SwiftCompilerPlugin | ||
import SwiftSyntaxMacros | ||
|
||
struct MyMacro: Macro { | ||
struct MyMacro_target_name: Macro { | ||
/// TODO: Implement one or more of the protocols that inherit | ||
/// from Macro. The appropriate macro protocol is determined | ||
/// by the "macro" declaration that MyMacro implements. | ||
/// by the "macro" declaration that MyMacro_target_name implements. | ||
/// Examples include: | ||
/// @freestanding(expression) macro --> ExpressionMacro | ||
/// @attached(member) macro --> MemberMacro | ||
} | ||
""", | ||
RelativePath("Sources/MyMacro/ProvidedMacros.swift") : """ | ||
RelativePath("Sources/MyMacro target-name/ProvidedMacros.swift") : """ | ||
import SwiftCompilerPlugin | ||
|
||
@main | ||
struct MyMacroMacros: CompilerPlugin { | ||
struct MyMacro_target_nameMacros: CompilerPlugin { | ||
let providingMacros: [Macro.Type] = [ | ||
MyMacro.self, | ||
MyMacro_target_name.self, | ||
] | ||
} | ||
""" | ||
] | ||
) { manifest in | ||
try AddTarget.addTarget( | ||
TargetDescription(name: "MyMacro", type: .macro), | ||
TargetDescription(name: "MyMacro target-name", type: .macro), | ||
to: manifest | ||
) | ||
} | ||
|
@@ -586,19 +586,19 @@ class ManifestEditTests: XCTestCase { | |
], | ||
targets: [ | ||
.testTarget( | ||
name: "MyTest", | ||
name: "MyTest target-name", | ||
dependencies: [ .product(name: "Testing", package: "swift-testing") ] | ||
), | ||
] | ||
) | ||
""", | ||
expectedAuxiliarySources: [ | ||
RelativePath("Tests/MyTest/MyTest.swift") : """ | ||
RelativePath("Tests/MyTest target-name/MyTest target-name.swift") : """ | ||
import Testing | ||
|
||
@Suite | ||
struct MyTestTests { | ||
@Test("MyTest tests") | ||
struct MyTest_target_nameTests { | ||
@Test("MyTest_target_name tests") | ||
func example() { | ||
#expect(42 == 17 + 25) | ||
} | ||
|
@@ -607,7 +607,7 @@ class ManifestEditTests: XCTestCase { | |
]) { manifest in | ||
try AddTarget.addTarget( | ||
TargetDescription( | ||
name: "MyTest", | ||
name: "MyTest target-name", | ||
type: .test | ||
), | ||
to: manifest, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How about using
c99name
instead?Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a very good point.
c99name
would certainly cover most edge cases, such as using dash-case, special characters, and digits in the first character. Well, it's already used for language-level target name sanitizing so it must be robust enough for this use case.The only remaining concern is that
c99
(obviously) doesn't cover Swift's reserved words. However, in that case, we could "just" escape the reserved word based on a list taken e.g. from swift-syntax, whichSwiftPM
already depends on.I believe combining these two safeguards should be a good enough way to proceed. What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm just visiting. :) There may be a better way to avoid reserved names—the package owners probably have ideas!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Alright :) @DougGregor, maybe you have some ideas about this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both of your ideas together (
cc9name
and using the swift-syntax keyword checking) sound reasonable to me.