Skip to content

Commit d374c74

Browse files
committed
Add a warning for TARGET_OS_* compilation conditions
1 parent fc03feb commit d374c74

File tree

3 files changed

+143
-1
lines changed

3 files changed

+143
-1
lines changed

Sources/SwiftIfConfig/IfConfigDiagnostic.swift

+24-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ enum IfConfigDiagnostic: Error, CustomStringConvertible {
3434
case ignoredTrailingComponents(version: VersionTuple, syntax: ExprSyntax)
3535
case integerLiteralCondition(syntax: ExprSyntax, replacement: Bool)
3636
case likelySimulatorPlatform(syntax: ExprSyntax)
37+
case likelyTargetOS(syntax: ExprSyntax, replacement: ExprSyntax?)
3738
case endiannessDoesNotMatch(syntax: ExprSyntax, argument: String)
3839
case macabiIsMacCatalyst(syntax: ExprSyntax)
3940
case expectedModuleName(syntax: ExprSyntax)
@@ -88,6 +89,12 @@ enum IfConfigDiagnostic: Error, CustomStringConvertible {
8889
return
8990
"platform condition appears to be testing for simulator environment; use 'targetEnvironment(simulator)' instead"
9091

92+
case .likelyTargetOS(syntax: _, replacement: let replacement?):
93+
return "'TARGET_OS_*' preprocessor macros are not available in Swift; use '\(replacement)' instead"
94+
95+
case .likelyTargetOS(syntax: _, replacement: nil):
96+
return "'TARGET_OS_*' preprocessor macros are not available in Swift; use 'os(...)' conditionals instead"
97+
9198
case .macabiIsMacCatalyst:
9299
return "'macabi' has been renamed to 'macCatalyst'"
93100

@@ -122,6 +129,7 @@ enum IfConfigDiagnostic: Error, CustomStringConvertible {
122129
.ignoredTrailingComponents(version: _, syntax: let syntax),
123130
.integerLiteralCondition(syntax: let syntax, replacement: _),
124131
.likelySimulatorPlatform(syntax: let syntax),
132+
.likelyTargetOS(syntax: let syntax, replacement: _),
125133
.endiannessDoesNotMatch(syntax: let syntax, argument: _),
126134
.macabiIsMacCatalyst(syntax: let syntax),
127135
.expectedModuleName(syntax: let syntax),
@@ -145,7 +153,7 @@ extension IfConfigDiagnostic: DiagnosticMessage {
145153
var severity: SwiftDiagnostics.DiagnosticSeverity {
146154
switch self {
147155
case .compilerVersionSecondComponentNotWildcard, .ignoredTrailingComponents,
148-
.likelySimulatorPlatform, .endiannessDoesNotMatch, .macabiIsMacCatalyst:
156+
.likelySimulatorPlatform, .likelyTargetOS, .endiannessDoesNotMatch, .macabiIsMacCatalyst:
149157
return .warning
150158
default: return .error
151159
}
@@ -192,6 +200,21 @@ extension IfConfigDiagnostic: DiagnosticMessage {
192200
)
193201
}
194202

203+
// For the likely TARGET_OS_* condition we may have a Fix-It.
204+
if case .likelyTargetOS(let syntax, let replacement?) = self {
205+
return Diagnostic(
206+
node: syntax,
207+
message: self,
208+
fixIt: .replace(
209+
message: SimpleFixItMessage(
210+
message: "replace with '\(replacement)'"
211+
),
212+
oldNode: syntax,
213+
newNode: replacement
214+
)
215+
)
216+
}
217+
195218
// For the targetEnvironment(macabi) -> macCatalyst rename we have a Fix-It.
196219
if case .macabiIsMacCatalyst(syntax: let syntax) = self {
197220
return Diagnostic(

Sources/SwiftIfConfig/IfConfigEvaluation.swift

+54
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ func evaluateIfConfig(
9999
if let identExpr = condition.as(DeclReferenceExprSyntax.self),
100100
let ident = identExpr.simpleIdentifier?.name
101101
{
102+
if let targetOSDiagnostic = diagnoseLikelyTargetOSTest(at: identExpr, name: ident) {
103+
extraDiagnostics.append(targetOSDiagnostic)
104+
}
102105
// Evaluate the custom condition. If the build configuration cannot answer this query, fail.
103106
return checkConfiguration(at: identExpr) {
104107
(active: try configuration.isCustomConditionSet(name: ident), syntaxErrorsAllowed: false)
@@ -627,6 +630,57 @@ private func diagnoseLikelySimulatorEnvironmentTest(
627630
return IfConfigDiagnostic.likelySimulatorPlatform(syntax: ExprSyntax(binOp)).asDiagnostic
628631
}
629632

633+
/// If this identifier looks like it is a `TARGET_OS_*` compilation condition,
634+
/// produce a diagnostic that suggests replacing it with the `os(*)` syntax.
635+
///
636+
/// For example, this checks for conditions like:
637+
///
638+
/// ```
639+
/// #if TARGET_OS_IOS
640+
/// ```
641+
///
642+
/// which should be replaced with
643+
///
644+
/// ```
645+
/// #if os(iOS)
646+
/// ```
647+
private func diagnoseLikelyTargetOSTest(
648+
at reference: DeclReferenceExprSyntax,
649+
name: String
650+
) -> Diagnostic? {
651+
let prefix = "TARGET_OS_"
652+
guard name.hasPrefix(prefix) else { return nil }
653+
let osName = String(name.dropFirst(prefix.count))
654+
655+
if unmappedTargetOSNames.contains(osName) {
656+
return IfConfigDiagnostic.likelyTargetOS(syntax: ExprSyntax(reference), replacement: nil).asDiagnostic
657+
}
658+
659+
guard let (function, argument) = targetOSNameMap[osName] else { return nil }
660+
let replacement = FunctionCallExprSyntax(callee: DeclReferenceExprSyntax(baseName: .identifier(function))) {
661+
LabeledExprSyntax(expression: DeclReferenceExprSyntax(baseName: .identifier(argument)))
662+
}
663+
664+
return IfConfigDiagnostic.likelyTargetOS(
665+
syntax: ExprSyntax(reference),
666+
replacement: ExprSyntax(replacement)
667+
).asDiagnostic
668+
}
669+
670+
// TARGET_OS_* macros that don’t have a direct Swift equivalent
671+
private let unmappedTargetOSNames = ["WIN32", "UNIX", "MAC", "IPHONE", "EMBEDDED"]
672+
private let targetOSNameMap: [String: (function: String, argument: String)] = [
673+
"WINDOWS": ("os", "Windows"),
674+
"LINUX": ("os", "Linux"),
675+
"OSX": ("os", "macOS"),
676+
"IOS": ("os", "iOS"),
677+
"MACCATALYST": ("targetEnvironment", "macCatalyst"),
678+
"TV": ("os", "tvOS"),
679+
"WATCH": ("os", "watchOS"),
680+
"VISION": ("os", "visionOS"),
681+
"SIMULATOR": ("targetEnvironment", "simulator"),
682+
]
683+
630684
extension IfConfigClauseSyntax {
631685
/// Fold the operators within an #if condition, turning sequence expressions
632686
/// involving the various allowed operators (&&, ||, !) into well-structured

Tests/SwiftIfConfigTest/EvaluateTests.swift

+65
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,71 @@ public class EvaluateTests: XCTestCase {
239239
)
240240
}
241241

242+
func testTargetOS() throws {
243+
assertIfConfig("TARGET_OS_DISHWASHER", .inactive)
244+
245+
assertIfConfig(
246+
"TARGET_OS_IOS",
247+
.inactive,
248+
diagnostics: [
249+
DiagnosticSpec(
250+
message: "'TARGET_OS_*' preprocessor macros are not available in Swift; use 'os(iOS)' instead",
251+
line: 1,
252+
column: 1,
253+
severity: .warning,
254+
fixIts: [
255+
FixItSpec(message: "replace with 'os(iOS)'")
256+
]
257+
)
258+
]
259+
)
260+
261+
assertIfConfig(
262+
"TARGET_OS_OSX",
263+
.inactive,
264+
diagnostics: [
265+
DiagnosticSpec(
266+
message: "'TARGET_OS_*' preprocessor macros are not available in Swift; use 'os(macOS)' instead",
267+
line: 1,
268+
column: 1,
269+
severity: .warning,
270+
fixIts: [
271+
FixItSpec(message: "replace with 'os(macOS)'")
272+
]
273+
)
274+
]
275+
)
276+
277+
assertIfConfig(
278+
"TARGET_OS_MACCATALYST",
279+
.inactive,
280+
diagnostics: [
281+
DiagnosticSpec(
282+
message: "'TARGET_OS_*' preprocessor macros are not available in Swift; use 'targetEnvironment(macCatalyst)' instead",
283+
line: 1,
284+
column: 1,
285+
severity: .warning,
286+
fixIts: [
287+
FixItSpec(message: "replace with 'targetEnvironment(macCatalyst)'")
288+
]
289+
)
290+
]
291+
)
292+
293+
assertIfConfig(
294+
"TARGET_OS_WIN32",
295+
.inactive,
296+
diagnostics: [
297+
DiagnosticSpec(
298+
message: "'TARGET_OS_*' preprocessor macros are not available in Swift; use 'os(...)' conditionals instead",
299+
line: 1,
300+
column: 1,
301+
severity: .warning
302+
)
303+
]
304+
)
305+
}
306+
242307
func testVersions() throws {
243308
assertIfConfig("swift(>=5.5)", .active)
244309
assertIfConfig("swift(<6)", .active)

0 commit comments

Comments
 (0)