Skip to content

Commit e1e67f1

Browse files
committed
NFC: Split out IfConfigError and the VersionTuple parsing into their own files
1 parent e649799 commit e1e67f1

File tree

3 files changed

+254
-227
lines changed

3 files changed

+254
-227
lines changed
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
import SwiftDiagnostics
13+
import SwiftSyntax
14+
15+
/// Describes the kinds of errors that can occur when processing #if conditions.
16+
enum IfConfigError: Error, CustomStringConvertible {
17+
case unknownExpression(ExprSyntax)
18+
case unhandledFunction(name: String, syntax: ExprSyntax)
19+
case requiresUnlabeledArgument(name: String, role: String, syntax: ExprSyntax)
20+
case unsupportedVersionOperator(name: String, operator: TokenSyntax)
21+
case invalidVersionOperand(name: String, syntax: ExprSyntax)
22+
case emptyVersionComponent(syntax: ExprSyntax)
23+
case compilerVersionOutOfRange(value: Int, upperLimit: Int, syntax: ExprSyntax)
24+
case compilerVersionSecondComponentNotWildcard(syntax: ExprSyntax)
25+
case compilerVersionTooManyComponents(syntax: ExprSyntax)
26+
case canImportMissingModule(syntax: ExprSyntax)
27+
case canImportLabel(syntax: ExprSyntax)
28+
case canImportTwoParameters(syntax: ExprSyntax)
29+
case ignoredTrailingComponents(version: VersionTuple, syntax: ExprSyntax)
30+
case integerLiteralCondition(syntax: ExprSyntax, replacement: Bool)
31+
32+
var description: String {
33+
switch self {
34+
case .unknownExpression:
35+
return "invalid conditional compilation expression"
36+
37+
case .unhandledFunction(name: let name, syntax: _):
38+
return "build configuration cannot handle '\(name)'"
39+
40+
case .requiresUnlabeledArgument(name: let name, role: let role, syntax: _):
41+
return "\(name) requires a single unlabeled argument for the \(role)"
42+
43+
case .unsupportedVersionOperator(name: let name, operator: let op):
44+
return "'\(name)' version check does not support operator '\(op.trimmedDescription)'"
45+
46+
case .invalidVersionOperand(name: let name, syntax: let version):
47+
return "'\(name)' version check has invalid version '\(version.trimmedDescription)'"
48+
49+
case .emptyVersionComponent(syntax: _):
50+
return "found empty version component"
51+
52+
case .compilerVersionOutOfRange(value: _, upperLimit: let upperLimit, syntax: _):
53+
// FIXME: This matches the C++ implementation, but it would be more useful to
54+
// provide the actual value as-written and avoid the mathy [0, N] syntax.
55+
return "compiler version component out of range: must be in [0, \(upperLimit)]"
56+
57+
case .compilerVersionSecondComponentNotWildcard(syntax: _):
58+
return "the second version component is not used for comparison in legacy compiler versions"
59+
60+
case .compilerVersionTooManyComponents(syntax: _):
61+
return "compiler version must not have more than five components"
62+
63+
case .canImportMissingModule(syntax: _):
64+
return "canImport requires a module name"
65+
66+
case .canImportLabel(syntax: _):
67+
return "2nd parameter of canImport should be labeled as _version or _underlyingVersion"
68+
69+
case .canImportTwoParameters(syntax: _):
70+
return "canImport can take only two parameters"
71+
72+
case .ignoredTrailingComponents(version: let version, syntax: _):
73+
return "trailing components of version '\(version.description)' are ignored"
74+
75+
case .integerLiteralCondition(syntax: let syntax, replacement: let replacement):
76+
return "'\(syntax.trimmedDescription)' is not a valid conditional compilation expression, use '\(replacement)'"
77+
}
78+
}
79+
80+
/// Retrieve the syntax node associated with this error.
81+
var syntax: Syntax {
82+
switch self {
83+
case .unknownExpression(let syntax),
84+
.unhandledFunction(name: _, syntax: let syntax),
85+
.requiresUnlabeledArgument(name: _, role: _, syntax: let syntax),
86+
.invalidVersionOperand(name: _, syntax: let syntax),
87+
.emptyVersionComponent(syntax: let syntax),
88+
.compilerVersionOutOfRange(value: _, upperLimit: _, syntax: let syntax),
89+
.compilerVersionTooManyComponents(syntax: let syntax),
90+
.compilerVersionSecondComponentNotWildcard(syntax: let syntax),
91+
.canImportMissingModule(syntax: let syntax),
92+
.canImportLabel(syntax: let syntax),
93+
.canImportTwoParameters(syntax: let syntax),
94+
.ignoredTrailingComponents(version: _, syntax: let syntax),
95+
.integerLiteralCondition(syntax: let syntax, replacement: _):
96+
return Syntax(syntax)
97+
98+
case .unsupportedVersionOperator(name: _, operator: let op):
99+
return Syntax(op)
100+
}
101+
}
102+
}
103+
104+
extension IfConfigError: DiagnosticMessage {
105+
var message: String { description }
106+
107+
var diagnosticID: MessageID {
108+
.init(domain: "SwiftIfConfig", id: "IfConfigError")
109+
}
110+
111+
var severity: SwiftDiagnostics.DiagnosticSeverity {
112+
switch self {
113+
case .ignoredTrailingComponents: return .warning
114+
default: return .error
115+
}
116+
}
117+
118+
private struct SimpleFixItMessage: FixItMessage {
119+
var message: String
120+
121+
var fixItID: MessageID {
122+
.init(domain: "SwiftIfConfig", id: "IfConfigFixIt")
123+
}
124+
}
125+
126+
var asDiagnostic: Diagnostic {
127+
// For the integer literal condition we have a Fix-It.
128+
if case .integerLiteralCondition(let syntax, let replacement) = self {
129+
return Diagnostic(
130+
node: syntax,
131+
message: self,
132+
fixIt: .replace(
133+
message: SimpleFixItMessage(
134+
message: "replace with Boolean literal '\(replacement)'"
135+
),
136+
oldNode: syntax,
137+
newNode: BooleanLiteralExprSyntax(
138+
literal: .keyword(replacement ? .true : .false)
139+
)
140+
)
141+
)
142+
}
143+
144+
return Diagnostic(node: syntax, message: self)
145+
}
146+
}

Sources/SwiftIfConfig/IfConfigEvaluation.swift

Lines changed: 3 additions & 227 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift.org open source project
44
//
5-
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See https://swift.org/LICENSE.txt for license information
@@ -13,231 +13,6 @@ import SwiftDiagnostics
1313
import SwiftOperators
1414
import SwiftSyntax
1515

16-
enum IfConfigError: Error, CustomStringConvertible {
17-
case unknownExpression(ExprSyntax)
18-
case unhandledFunction(name: String, syntax: ExprSyntax)
19-
case requiresUnlabeledArgument(name: String, role: String, syntax: ExprSyntax)
20-
case unsupportedVersionOperator(name: String, operator: TokenSyntax)
21-
case invalidVersionOperand(name: String, syntax: ExprSyntax)
22-
case emptyVersionComponent(syntax: ExprSyntax)
23-
case compilerVersionOutOfRange(value: Int, upperLimit: Int, syntax: ExprSyntax)
24-
case compilerVersionSecondComponentNotWildcard(syntax: ExprSyntax)
25-
case compilerVersionTooManyComponents(syntax: ExprSyntax)
26-
case canImportMissingModule(syntax: ExprSyntax)
27-
case canImportLabel(syntax: ExprSyntax)
28-
case canImportTwoParameters(syntax: ExprSyntax)
29-
case ignoredTrailingComponents(version: VersionTuple, syntax: ExprSyntax)
30-
case integerLiteralCondition(syntax: ExprSyntax, replacement: Bool)
31-
32-
var description: String {
33-
switch self {
34-
case .unknownExpression:
35-
return "invalid conditional compilation expression"
36-
37-
case .unhandledFunction(name: let name, syntax: _):
38-
return "build configuration cannot handle '\(name)'"
39-
40-
case .requiresUnlabeledArgument(name: let name, role: let role, syntax: _):
41-
return "\(name) requires a single unlabeled argument for the \(role)"
42-
43-
case .unsupportedVersionOperator(name: let name, operator: let op):
44-
return "'\(name)' version check does not support operator '\(op.trimmedDescription)'"
45-
46-
case .invalidVersionOperand(name: let name, syntax: let version):
47-
return "'\(name)' version check has invalid version '\(version.trimmedDescription)'"
48-
49-
case .emptyVersionComponent(syntax: _):
50-
return "found empty version component"
51-
52-
case .compilerVersionOutOfRange(value: _, upperLimit: let upperLimit, syntax: _):
53-
// FIXME: This matches the C++ implementation, but it would be more useful to
54-
// provide the actual value as-written and avoid the mathy [0, N] syntax.
55-
return "compiler version component out of range: must be in [0, \(upperLimit)]"
56-
57-
case .compilerVersionSecondComponentNotWildcard(syntax: _):
58-
return "the second version component is not used for comparison in legacy compiler versions"
59-
60-
case .compilerVersionTooManyComponents(syntax: _):
61-
return "compiler version must not have more than five components"
62-
63-
case .canImportMissingModule(syntax: _):
64-
return "canImport requires a module name"
65-
66-
case .canImportLabel(syntax: _):
67-
return "2nd parameter of canImport should be labeled as _version or _underlyingVersion"
68-
69-
case .canImportTwoParameters(syntax: _):
70-
return "canImport can take only two parameters"
71-
72-
case .ignoredTrailingComponents(version: let version, syntax: _):
73-
return "trailing components of version '\(version.description)' are ignored"
74-
75-
case .integerLiteralCondition(syntax: let syntax, replacement: let replacement):
76-
return "'\(syntax.trimmedDescription)' is not a valid conditional compilation expression, use '\(replacement)'"
77-
}
78-
}
79-
80-
/// Retrieve the syntax node associated with this error.
81-
var syntax: Syntax {
82-
switch self {
83-
case .unknownExpression(let syntax),
84-
.unhandledFunction(name: _, syntax: let syntax),
85-
.requiresUnlabeledArgument(name: _, role: _, syntax: let syntax),
86-
.invalidVersionOperand(name: _, syntax: let syntax),
87-
.emptyVersionComponent(syntax: let syntax),
88-
.compilerVersionOutOfRange(value: _, upperLimit: _, syntax: let syntax),
89-
.compilerVersionTooManyComponents(syntax: let syntax),
90-
.compilerVersionSecondComponentNotWildcard(syntax: let syntax),
91-
.canImportMissingModule(syntax: let syntax),
92-
.canImportLabel(syntax: let syntax),
93-
.canImportTwoParameters(syntax: let syntax),
94-
.ignoredTrailingComponents(version: _, syntax: let syntax),
95-
.integerLiteralCondition(syntax: let syntax, replacement: _):
96-
return Syntax(syntax)
97-
98-
case .unsupportedVersionOperator(name: _, operator: let op):
99-
return Syntax(op)
100-
}
101-
}
102-
}
103-
104-
extension IfConfigError: DiagnosticMessage {
105-
var message: String { description }
106-
107-
var diagnosticID: MessageID {
108-
.init(domain: "SwiftIfConfig", id: "IfConfigError")
109-
}
110-
111-
var severity: SwiftDiagnostics.DiagnosticSeverity {
112-
switch self {
113-
case .ignoredTrailingComponents: return .warning
114-
default: return .error
115-
}
116-
}
117-
118-
private struct SimpleFixItMessage: FixItMessage {
119-
var message: String
120-
121-
var fixItID: MessageID {
122-
.init(domain: "SwiftIfConfig", id: "IfConfigFixIt")
123-
}
124-
}
125-
126-
var asDiagnostic: Diagnostic {
127-
// For the integer literal condition we have a Fix-It.
128-
if case .integerLiteralCondition(let syntax, let replacement) = self {
129-
return Diagnostic(
130-
node: syntax,
131-
message: self,
132-
fixIt: .replace(
133-
message: SimpleFixItMessage(
134-
message: "replace with Boolean literal '\(replacement)'"
135-
),
136-
oldNode: syntax,
137-
newNode: BooleanLiteralExprSyntax(
138-
literal: .keyword(replacement ? .true : .false)
139-
)
140-
)
141-
)
142-
}
143-
144-
return Diagnostic(node: syntax, message: self)
145-
}
146-
}
147-
148-
extension VersionTuple {
149-
/// Parse a compiler build version of the form "5007.*.1.2.3*", which is
150-
/// used by an older if configuration form `_compiler_version("...")`.
151-
/// - Parameters:
152-
/// - versionString: The version string for the compiler build version that
153-
/// we are parsing.
154-
/// - versionSyntax: The syntax node that contains the version string, used
155-
/// only for diagnostic purposes.
156-
fileprivate init(
157-
parsingCompilerBuildVersion versionString: String,
158-
_ versionSyntax: ExprSyntax
159-
) throws {
160-
components = []
161-
162-
// Version value are separated by periods.
163-
let componentStrings = versionString.split(separator: ".")
164-
165-
/// Record a component after checking its value.
166-
func recordComponent(_ value: Int) throws {
167-
let limit = components.isEmpty ? 9223371 : 999
168-
if value < 0 || value > limit {
169-
throw IfConfigError.compilerVersionOutOfRange(value: value, upperLimit: limit, syntax: versionSyntax)
170-
}
171-
172-
components.append(value)
173-
}
174-
175-
// Parse the components into version values.
176-
for (index, componentString) in componentStrings.enumerated() {
177-
// Check ahead of time for empty version components
178-
if componentString.isEmpty {
179-
throw IfConfigError.emptyVersionComponent(syntax: versionSyntax)
180-
}
181-
182-
// The second component is always "*", and is never used for comparison.
183-
if index == 1 {
184-
if componentString != "*" {
185-
throw IfConfigError.compilerVersionSecondComponentNotWildcard(syntax: versionSyntax)
186-
}
187-
try recordComponent(0)
188-
continue
189-
}
190-
191-
// Every other component must be an integer value.
192-
guard let component = Int(componentString) else {
193-
throw IfConfigError.invalidVersionOperand(name: "_compiler_version", syntax: versionSyntax)
194-
}
195-
196-
try recordComponent(component)
197-
}
198-
199-
// Only allowed to specify up to 5 version components.
200-
if components.count > 5 {
201-
throw IfConfigError.compilerVersionTooManyComponents(syntax: versionSyntax)
202-
}
203-
204-
// In the beginning, '_compiler_version(string-literal)' was designed for a
205-
// different version scheme where the major was fairly large and the minor
206-
// was ignored; now we use one where the minor is significant and major and
207-
// minor match the Swift language version. Specifically, majors 600-1300
208-
// were used for Swift 1.0-5.5 (based on clang versions), but then we reset
209-
// the numbering based on Swift versions, so 5.6 had major 5. We assume
210-
// that majors below 600 use the new scheme and equal/above it use the old
211-
// scheme.
212-
//
213-
// However, we want the string literal variant of '_compiler_version' to
214-
// maintain source compatibility with old checks; that means checks for new
215-
// versions have to be written so that old compilers will think they represent
216-
// newer versions, while new compilers have to interpret old version number
217-
// strings in a way that will compare correctly to the new versions compiled
218-
// into them.
219-
//
220-
// To achieve this, modern compilers divide the major by 1000 and overwrite
221-
// the wildcard component with the remainder, effectively shifting the last
222-
// three digits of the major into the minor, before comparing it to the
223-
// compiler version:
224-
//
225-
// _compiler_version("5007.*.1.2.3") -> 5.7.1.2.3
226-
// _compiler_version("1300.*.1.2.3") -> 1.300.1.2.3 (smaller than 5.6)
227-
// _compiler_version( "600.*.1.2.3") -> 0.600.1.2.3 (smaller than 5.6)
228-
//
229-
// So if you want to specify a 5.7.z.a.b version, we ask users to either
230-
// write it as 5007.*.z.a.b, or to use the new 'compiler(>= version)'
231-
// syntax instead, which does not perform this conversion.
232-
if !components.isEmpty {
233-
if components.count > 1 {
234-
components[1] = components[0] % 1000
235-
}
236-
components[0] = components[0] / 1000
237-
}
238-
}
239-
}
240-
24116
/// Evaluate the condition of an `#if`.
24217
/// - Parameters:
24318
/// - condition: The condition to evaluate, which we assume has already been
@@ -247,7 +22,8 @@ extension VersionTuple {
24722
/// - diagnosticHandler: Receives any diagnostics that are produced by the
24823
/// evaluation, whether from errors in the source code or produced by the
24924
/// build configuration itself.
250-
/// - Throws: Throws if an error occurs occur during evaluation. The error will
25+
/// - Throws: Throws if an error occurs occur during evaluation that prevents
26+
/// this function from forming a valid result. The error will
25127
/// also be provided to the diagnostic handler before doing so.
25228
/// - Returns: A pair of Boolean values. The first describes whether the
25329
/// condition holds with the given build configuration. The second whether

0 commit comments

Comments
 (0)