Skip to content

Commit e4f5517

Browse files
committed
Implement warning to recognize conditions that should be simulator checks
Implement a warning to recognize `#if` conditions like this: ```swift ((os(iOS) || os(tvOS)) && (arch(i386) || arch(x86_64))) ``` and suggest `targetEnvironment(simultator)` instead.
1 parent 9fdc7f4 commit e4f5517

File tree

5 files changed

+149
-7
lines changed

5 files changed

+149
-7
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ let package = Package(
143143

144144
.target(
145145
name: "SwiftIfConfig",
146-
dependencies: ["SwiftSyntax", "SwiftDiagnostics", "SwiftOperators"],
146+
dependencies: ["SwiftSyntax", "SwiftSyntaxBuilder", "SwiftDiagnostics", "SwiftOperators"],
147147
exclude: ["CMakeLists.txt"]
148148
),
149149

Sources/SwiftIfConfig/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@ add_swift_syntax_library(SwiftIfConfig
2424

2525
target_link_swift_syntax_libraries(SwiftIfConfig PUBLIC
2626
SwiftSyntax
27+
SwiftSyntaxBuilder
2728
SwiftDiagnostics
2829
SwiftOperators)

Sources/SwiftIfConfig/IfConfigError.swift

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import SwiftDiagnostics
1414
import SwiftSyntax
15+
import SwiftSyntaxBuilder
1516

1617
/// Describes the kinds of errors that can occur when processing #if conditions.
1718
enum IfConfigError: Error, CustomStringConvertible {
@@ -29,6 +30,7 @@ enum IfConfigError: Error, CustomStringConvertible {
2930
case canImportTwoParameters(syntax: ExprSyntax)
3031
case ignoredTrailingComponents(version: VersionTuple, syntax: ExprSyntax)
3132
case integerLiteralCondition(syntax: ExprSyntax, replacement: Bool)
33+
case likelySimulatorPlatform(syntax: ExprSyntax)
3234

3335
var description: String {
3436
switch self {
@@ -75,6 +77,10 @@ enum IfConfigError: Error, CustomStringConvertible {
7577

7678
case .integerLiteralCondition(syntax: let syntax, replacement: let replacement):
7779
return "'\(syntax.trimmedDescription)' is not a valid conditional compilation expression, use '\(replacement)'"
80+
81+
case .likelySimulatorPlatform:
82+
return
83+
"platform condition appears to be testing for simulator environment; use 'targetEnvironment(simulator)' instead"
7884
}
7985
}
8086

@@ -93,7 +99,8 @@ enum IfConfigError: Error, CustomStringConvertible {
9399
.canImportLabel(syntax: let syntax),
94100
.canImportTwoParameters(syntax: let syntax),
95101
.ignoredTrailingComponents(version: _, syntax: let syntax),
96-
.integerLiteralCondition(syntax: let syntax, replacement: _):
102+
.integerLiteralCondition(syntax: let syntax, replacement: _),
103+
.likelySimulatorPlatform(syntax: let syntax):
97104
return Syntax(syntax)
98105

99106
case .unsupportedVersionOperator(name: _, operator: let op):
@@ -111,7 +118,7 @@ extension IfConfigError: DiagnosticMessage {
111118

112119
var severity: SwiftDiagnostics.DiagnosticSeverity {
113120
switch self {
114-
case .ignoredTrailingComponents: return .warning
121+
case .ignoredTrailingComponents, .likelySimulatorPlatform: return .warning
115122
default: return .error
116123
}
117124
}
@@ -142,6 +149,21 @@ extension IfConfigError: DiagnosticMessage {
142149
)
143150
}
144151

152+
// For the likely targetEnvironment(simulator) condition we have a Fix-It.
153+
if case .likelySimulatorPlatform(let syntax) = self {
154+
return Diagnostic(
155+
node: syntax,
156+
message: self,
157+
fixIt: .replace(
158+
message: SimpleFixItMessage(
159+
message: "replace with 'targetEnvironment(simulator)'"
160+
),
161+
oldNode: syntax,
162+
newNode: "targetEnvironment(simulator)" as ExprSyntax
163+
)
164+
)
165+
}
166+
145167
return Diagnostic(node: syntax, message: self)
146168
}
147169
}

Sources/SwiftIfConfig/IfConfigEvaluation.swift

Lines changed: 99 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@ func evaluateIfConfig(
123123
let op = binOp.operator.as(BinaryOperatorExprSyntax.self),
124124
(op.operator.text == "&&" || op.operator.text == "||")
125125
{
126+
// Check whether this was likely to be a check for targetEnvironment(simulator).
127+
if let targetEnvironmentDiag = diagnoseLikelySimulatorEnvironmentTest(binOp) {
128+
extraDiagnostics.append(targetEnvironmentDiag)
129+
}
130+
126131
// Evaluate the left-hand side.
127132
let (lhsActive, lhssyntaxErrorsAllowed, lhsDiagnostics) = evaluateIfConfig(
128133
condition: binOp.leftOperand,
@@ -134,9 +139,17 @@ func evaluateIfConfig(
134139
if lhssyntaxErrorsAllowed {
135140
switch (lhsActive, op.operator.text) {
136141
case (true, "||"):
137-
return (active: true, syntaxErrorsAllowed: lhssyntaxErrorsAllowed, diagnostics: lhsDiagnostics)
142+
return (
143+
active: true,
144+
syntaxErrorsAllowed: lhssyntaxErrorsAllowed,
145+
diagnostics: extraDiagnostics + lhsDiagnostics
146+
)
138147
case (false, "&&"):
139-
return (active: false, syntaxErrorsAllowed: lhssyntaxErrorsAllowed, diagnostics: lhsDiagnostics)
148+
return (
149+
active: false,
150+
syntaxErrorsAllowed: lhssyntaxErrorsAllowed,
151+
diagnostics: extraDiagnostics + lhsDiagnostics
152+
)
140153
default:
141154
break
142155
}
@@ -153,14 +166,14 @@ func evaluateIfConfig(
153166
return (
154167
active: lhsActive || rhsActive,
155168
syntaxErrorsAllowed: lhssyntaxErrorsAllowed && rhssyntaxErrorsAllowed,
156-
diagnostics: lhsDiagnostics + rhsDiagnostics
169+
diagnostics: extraDiagnostics + lhsDiagnostics + rhsDiagnostics
157170
)
158171

159172
case "&&":
160173
return (
161174
active: lhsActive && rhsActive,
162175
syntaxErrorsAllowed: lhssyntaxErrorsAllowed || rhssyntaxErrorsAllowed,
163-
diagnostics: lhsDiagnostics + rhsDiagnostics
176+
diagnostics: extraDiagnostics + lhsDiagnostics + rhsDiagnostics
164177
)
165178

166179
default:
@@ -433,6 +446,88 @@ func evaluateIfConfig(
433446
return recordError(.unknownExpression(condition))
434447
}
435448

449+
/// Determine whether the given condition only involves disjunctions that
450+
/// check the given config function against one of the provided values.
451+
///
452+
/// For example, this will match a condition like `os(iOS) || os(tvOS)`
453+
/// when passed `IfConfigFunctions.os` and `["iOS", "tvOS"]`.
454+
private func isConditionDisjunction(
455+
_ condition: some ExprSyntaxProtocol,
456+
function: IfConfigFunctions,
457+
anyOf values: [String]
458+
) -> Bool {
459+
// Recurse into disjunctions. Both sides need to match.
460+
if let binOp = condition.as(InfixOperatorExprSyntax.self),
461+
let op = binOp.operator.as(BinaryOperatorExprSyntax.self),
462+
op.operator.text == "||"
463+
{
464+
return isConditionDisjunction(binOp.leftOperand, function: function, anyOf: values)
465+
&& isConditionDisjunction(binOp.rightOperand, function: function, anyOf: values)
466+
}
467+
468+
// Look through parentheses.
469+
if let tuple = condition.as(TupleExprSyntax.self), tuple.isParentheses,
470+
let element = tuple.elements.first
471+
{
472+
return isConditionDisjunction(element.expression, function: function, anyOf: values)
473+
}
474+
475+
// If we have a call to this function, check whether the argument is one of
476+
// the acceptable values.
477+
if let call = condition.as(FunctionCallExprSyntax.self),
478+
let fnName = call.calledExpression.simpleIdentifierExpr,
479+
let callFn = IfConfigFunctions(rawValue: fnName),
480+
callFn == function,
481+
let argExpr = call.arguments.singleUnlabeledExpression,
482+
let arg = argExpr.simpleIdentifierExpr
483+
{
484+
return values.contains(arg)
485+
}
486+
487+
return false
488+
}
489+
490+
/// If this binary operator looks like it could be replaced by a
491+
/// targetEnvironment(simulator) check, produce a diagnostic that does so.
492+
///
493+
/// For example, this checks for conditions like:
494+
///
495+
/// ```
496+
/// #if (os(iOS) || os(tvOS)) && (arch(i386) || arch(x86_64))
497+
/// ```
498+
///
499+
/// which should be replaced with
500+
///
501+
/// ```
502+
/// #if targetEnvironment(simulator)
503+
/// ```
504+
private func diagnoseLikelySimulatorEnvironmentTest(
505+
_ binOp: InfixOperatorExprSyntax
506+
) -> Diagnostic? {
507+
guard let op = binOp.operator.as(BinaryOperatorExprSyntax.self),
508+
op.operator.text == "&&"
509+
else {
510+
return nil
511+
}
512+
513+
func isSimulatorPlatformOSTest(_ condition: ExprSyntax) -> Bool {
514+
return isConditionDisjunction(condition, function: .os, anyOf: ["iOS", "tvOS", "watchOS"])
515+
}
516+
517+
func isSimulatorPlatformArchTest(_ condition: ExprSyntax) -> Bool {
518+
return isConditionDisjunction(condition, function: .arch, anyOf: ["i386", "x86_64"])
519+
}
520+
521+
guard
522+
(isSimulatorPlatformOSTest(binOp.leftOperand) && isSimulatorPlatformArchTest(binOp.rightOperand))
523+
|| (isSimulatorPlatformOSTest(binOp.rightOperand) && isSimulatorPlatformArchTest(binOp.leftOperand))
524+
else {
525+
return nil
526+
}
527+
528+
return IfConfigError.likelySimulatorPlatform(syntax: ExprSyntax(binOp)).asDiagnostic
529+
}
530+
436531
extension IfConfigClauseSyntax {
437532
/// Fold the operators within an #if condition, turning sequence expressions
438533
/// involving the various allowed operators (&&, ||, !) into well-structured

Tests/SwiftIfConfigTest/EvaluateTests.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,30 @@ public class EvaluateTests: XCTestCase {
278278
]
279279
)
280280
}
281+
282+
func testLikelySimulatorEnvironment() throws {
283+
assertIfConfig(
284+
"((os(iOS) || os(tvOS)) && (arch(i386) || arch(x86_64))) && DEBUG",
285+
.inactive,
286+
diagnostics: [
287+
DiagnosticSpec(
288+
message:
289+
"platform condition appears to be testing for simulator environment; use 'targetEnvironment(simulator)' instead",
290+
line: 1,
291+
column: 2,
292+
severity: .warning,
293+
fixIts: [
294+
FixItSpec(message: "replace with 'targetEnvironment(simulator)'")
295+
]
296+
)
297+
]
298+
)
299+
300+
assertIfConfig(
301+
"((os(iOS) || os(tvOS)) && (arch(arm64) || arch(x86_64))) && DEBUG",
302+
.inactive
303+
)
304+
}
281305
}
282306

283307
/// Assert the results of evaluating the condition within an `#if` against the

0 commit comments

Comments
 (0)