Skip to content

Commit 23637ba

Browse files
committed
Add a function to check if a name can be used as an identifier in a given context
rdar://120721971
1 parent 4265ac0 commit 23637ba

File tree

4 files changed

+188
-0
lines changed

4 files changed

+188
-0
lines changed

Release Notes/511.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@
3232
- Description: The `throwsSpecifier` for the effects nodes (`AccessorEffectSpecifiers`, `FunctionEffectSpecifiers`, `TypeEffectSpecifiers`, `EffectSpecifiers`) has been replaced with `throwsClause`, which captures both the throws specifier and the (optional) thrown error type, as introduced by SE-0413.
3333
- Pull Request: https://github.com/apple/swift-syntax/pull/2379
3434

35+
- `isValidIdentifier(_:,for:)`
36+
- Description: Adds a new API to check if a name can be used as an identifier in a given context.
37+
- Pull Request: https://github.com/apple/swift-syntax/pull/2434
38+
3539
## API Behavior Changes
3640

3741
## Deprecations

Sources/SwiftParser/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ add_swift_syntax_library(SwiftParser
1515
Directives.swift
1616
Expressions.swift
1717
IncrementalParseTransition.swift
18+
IsValidIdentifier.swift
1819
Lookahead.swift
1920
LoopProgressCondition.swift
2021
Modifiers.swift
Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 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+
13+
import SwiftSyntax
14+
15+
/// Context in which to check if a name can be used as an identifier.
16+
///
17+
/// - SeeAlso: ``SwiftParser/isValidIdentifier(_:for:)``
18+
public enum IdentifierCheckContext {
19+
/// Check if a name can be used to declare a variable, ie. if it can be used after a `let` or `var` keyword.
20+
///
21+
/// ### Examples
22+
/// - `test` is a valid variable name and `let test: Int` is valid Swift code
23+
/// - `class` is not a valid variable and `let class: Int` is invalid Swift code
24+
case variableName
25+
26+
/// Check if a name can be used as a member access, ie. if it can be used after a `.`.
27+
///
28+
/// ### Examples
29+
/// - `test` is a valid identifier for member access because `myStruct.test` is valid
30+
/// - `class` is a valid identifier for member access because `myStruct.class` is valid, even though `class`
31+
/// needs to be wrapped in backticks when used to declare a variable.
32+
/// - `self` is not a valid identifier for member access because `myStruct.self` does not access a member named
33+
/// `self` on `myStruct` and instead returns `myStruct` itself.
34+
case memberAccess
35+
}
36+
37+
/// Checks whether `name` can be used as an identifier in a certain context.
38+
///
39+
/// If the name cannot be used as an identifier in this context, it needs to be escaped.
40+
///
41+
/// For example, `class` is not a valid identifier for a variable name and needs to be be wrapped in backticks
42+
/// to be valid Swift code, like the following.
43+
///
44+
/// ```swift
45+
/// let `class`: String
46+
/// ```
47+
///
48+
/// The context is important here – some names can be used as identifiers in some contexts but not others.
49+
/// For example, `myStruct.class` is valid without adding backticks `class`, but as mentioned above,
50+
/// backticks need to be added when `class` is used as a variable name.
51+
///
52+
/// - SeeAlso: ``SwiftParser/IdentifierCheckContext``
53+
public func isValidIdentifier(_ name: String, for context: IdentifierCheckContext) -> Bool {
54+
switch context {
55+
case .variableName:
56+
return isValidVariableName(name)
57+
case .memberAccess:
58+
return isValidMemberAccess(name)
59+
}
60+
}
61+
62+
private func isValidVariableName(_ name: String) -> Bool {
63+
var parser = Parser("var \(name)")
64+
let decl = DeclSyntax.parse(from: &parser)
65+
guard parser.at(.endOfFile) else {
66+
// We didn't parse the entire name. Probably some garbage left in the name, so not an identifier.
67+
return false
68+
}
69+
guard !decl.hasError && !decl.hasWarning else {
70+
// There were syntax errors in the source code. So not valid.
71+
return false
72+
}
73+
guard let variable = decl.as(VariableDeclSyntax.self) else {
74+
return false
75+
}
76+
guard Array(variable.tokens(viewMode: .sourceAccurate)).count == 2 else {
77+
// We expect to parse exactly two tokens ('var' and the name).
78+
// If we parsed more, the name got parsed as two tokens and is thus not a
79+
// valid name.
80+
return false
81+
}
82+
guard let identifier = variable.bindings.first?.pattern.as(IdentifierPatternSyntax.self)?.identifier else {
83+
return false
84+
}
85+
guard identifier.tokenKind == .identifier(name) else {
86+
// We parsed the name as a keyword, eg. `self`, so not a valid identifier.
87+
return false
88+
}
89+
guard identifier.leadingTrivia.isEmpty && identifier.trailingTrivia.isEmpty else {
90+
// The name contained whitespace, which isn't valid in an identifier.
91+
return false
92+
}
93+
return true
94+
}
95+
96+
private func isValidMemberAccess(_ name: String) -> Bool {
97+
var parser = Parser("t.\(name)")
98+
let expr = ExprSyntax.parse(from: &parser)
99+
guard parser.at(.endOfFile) else {
100+
// We didn't parse the entire name. Probably some garbage left in the name, so not an identifier.
101+
return false
102+
}
103+
guard !expr.hasError && !expr.hasWarning else {
104+
// There were syntax errors in the source code. So not valid.
105+
return false
106+
}
107+
guard let memberAccess = expr.as(MemberAccessExprSyntax.self) else {
108+
return false
109+
}
110+
guard Array(memberAccess.tokens(viewMode: .sourceAccurate)).count == 3 else {
111+
// We expect to parse exactly two tokens ('t', '.', and the name).
112+
// If we parsed more, the name got parsed as two tokens and is thus not a
113+
// valid name.
114+
return false
115+
}
116+
let identifier = memberAccess.declName.baseName
117+
guard identifier.tokenKind == .identifier(name) else {
118+
// We parsed the name as a keyword, eg. `self`, so not a valid identifier.
119+
return false
120+
}
121+
guard identifier.leadingTrivia.isEmpty && identifier.trailingTrivia.isEmpty else {
122+
// The name contained whitespace, which isn't valid in an identifier.
123+
return false
124+
}
125+
return true
126+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2014 - 2023 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+
13+
import SwiftParser
14+
import XCTest
15+
16+
/// Defines whether a name is expected to be a valid identifier in the given contexts.
17+
private struct ValidIdentifierSpec: ExpressibleByBooleanLiteral {
18+
let variableName: Bool
19+
let memberAccess: Bool
20+
21+
init(variableName: Bool, memberAccess: Bool) {
22+
self.variableName = variableName
23+
self.memberAccess = memberAccess
24+
}
25+
26+
init(booleanLiteral value: BooleanLiteralType) {
27+
self.init(variableName: value, memberAccess: value)
28+
}
29+
}
30+
31+
private func assertValidIdentifier(
32+
_ name: String,
33+
_ spec: ValidIdentifierSpec,
34+
file: StaticString = #file,
35+
line: UInt = #line
36+
) {
37+
XCTAssertEqual(isValidIdentifier(name, for: .variableName), spec.variableName, "Checking identifier for variableName context", file: file, line: line)
38+
XCTAssertEqual(isValidIdentifier(name, for: .memberAccess), spec.memberAccess, "Checking identifier for memberAccess context", file: file, line: line)
39+
}
40+
41+
class IsValidIdentifierTests: XCTestCase {
42+
func testIsValidIdentifier() {
43+
assertValidIdentifier("test", true)
44+
assertValidIdentifier("class", ValidIdentifierSpec(variableName: false, memberAccess: true))
45+
assertValidIdentifier("`class`", true)
46+
assertValidIdentifier("self", false)
47+
assertValidIdentifier("`self`", true)
48+
assertValidIdentifier("let", ValidIdentifierSpec(variableName: false, memberAccess: true))
49+
assertValidIdentifier("`let`", true)
50+
assertValidIdentifier("", false)
51+
assertValidIdentifier("test: Int", false)
52+
assertValidIdentifier("test ", false)
53+
assertValidIdentifier("test\0", false)
54+
assertValidIdentifier("test\0test", false)
55+
assertValidIdentifier("test(x:)", false)
56+
}
57+
}

0 commit comments

Comments
 (0)