Skip to content

Commit f24b22a

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 f24b22a

File tree

4 files changed

+101
-0
lines changed

4 files changed

+101
-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: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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: ``isValidIdentifier``
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+
27+
/// Checks whether `name` can be used as an identifier in a certain context.
28+
///
29+
/// If the name cannot be used as an identifier in this context, it needs to be escaped.
30+
/// For example, `class` is not a valid identifier for a variable name and needs to be spelled as ```let `class`: String``` to be valid Swift code.
31+
///
32+
/// The context is important here some names can be used as identifiers in some contexts but not others. For example, `myStruct.class` is valid without adding backticks `class`, but as mentioned above, backticks need to be added when `class` is used as a variable name.
33+
///
34+
/// - SeeAlso: ``IdentifierCheckContext``
35+
public func isValidIdentifier(_ name: String, for context: IdentifierCheckContext) -> Bool {
36+
switch context {
37+
case .variableName:
38+
return isValidVariableName(name)
39+
}
40+
}
41+
42+
private func isValidVariableName(_ name: String) -> Bool {
43+
var parser = Parser("var \(name)")
44+
let decl = DeclSyntax.parse(from: &parser)
45+
guard parser.at(.endOfFile) else {
46+
// We didn't parse the entire name. Probably some garbage left in the name, so not an identifier.
47+
return false
48+
}
49+
guard let variable = decl.as(VariableDeclSyntax.self) else {
50+
return false
51+
}
52+
guard Array(variable.tokens(viewMode: .sourceAccurate)).count == 2 else {
53+
// We expect to parse exactly two tokens ('var' and the name).
54+
// If we parsed more, the name go parsed as two tokens and is thus not a
55+
// valid name.
56+
return false
57+
}
58+
guard let identifier = variable.bindings.first?.pattern.as(IdentifierPatternSyntax.self)?.identifier else {
59+
return false
60+
}
61+
guard identifier.tokenKind == .identifier(name) else {
62+
// We parsed the name as a keyword, eg. `self`, so not a valid identifier.
63+
return false
64+
}
65+
guard identifier.leadingTrivia.isEmpty && identifier.trailingTrivia.isEmpty else {
66+
// The name contained whitespace, which isn't valid in an identifier.
67+
return false
68+
}
69+
return true
70+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
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+
class IsValidIdentifierTests: XCTestCase {
17+
func testIsValidVariableName() {
18+
XCTAssertTrue(isValidIdentifier("test", for: .variableName))
19+
XCTAssertFalse(isValidIdentifier("self", for: .variableName))
20+
XCTAssertTrue(isValidIdentifier("`self`", for: .variableName))
21+
XCTAssertTrue(isValidIdentifier("`let`", for: .variableName))
22+
XCTAssertFalse(isValidIdentifier("", for: .variableName))
23+
XCTAssertFalse(isValidIdentifier("test: Int", for: .variableName))
24+
XCTAssertFalse(isValidIdentifier("test ", for: .variableName))
25+
}
26+
}

0 commit comments

Comments
 (0)