diff --git a/Release Notes/510.md b/Release Notes/510.md index 80baa4c1844..97a8aebb7af 100644 --- a/Release Notes/510.md +++ b/Release Notes/510.md @@ -5,13 +5,16 @@ - `SyntaxStringInterpolation.appendInterpolation(_: (some SyntaxProtocol)?)` - Description: Allows optional syntax nodes to be used inside string interpolation of syntax nodes. If the node is `nil`, nothing will get added to the string interpolation. - Pull Request: https://github.com/apple/swift-syntax/pull/2085 + - `SyntaxCollection.index(at:)` - Description: Returns the index of the n-th element in a `SyntaxCollection`. This computation is in O(n) and `SyntaxCollection` is not subscriptable by an integer. - Pull Request: https://github.com/apple/swift-syntax/pull/2014 + - Convenience initializer `ClosureCaptureSyntax.init()` - Description: Provides a convenience initializer for `ClosureCaptureSyntax` that takes a concrete `name` argument and automatically adds `equal = TokenSyntax.equalToken()` to it. - Issue: https://github.com/apple/swift-syntax/issues/1984 - Pull Request: https://github.com/apple/swift-syntax/pull/2127 + - Convenience initializer `EnumCaseParameterSyntax.init()` - Description: Provides a convenience initializer for `EnumCaseParameterSyntax` that takes a concrete `firstName` value and adds `colon = TokenSyntax.colonToken()` automatically to it. - Issue: https://github.com/apple/swift-syntax/issues/1984 @@ -30,7 +33,7 @@ - Issue: https://github.com/apple/swift-syntax/issues/2092 - Pull Request: https://github.com/apple/swift-syntax/pull/2108 -- Same-Type Casts +- Same-Type Casts - Description: `is`, `as`, and `cast` overloads on `SyntaxProtocol` with same-type conversions are marked as deprecated. The deprecated methods emit a warning indicating the cast will always succeed. - Issue: https://github.com/apple/swift-syntax/issues/2092 - Pull Request: https://github.com/apple/swift-syntax/pull/2108 diff --git a/Release Notes/601.md b/Release Notes/601.md index eee0be858b2..a26d42cde2e 100644 --- a/Release Notes/601.md +++ b/Release Notes/601.md @@ -2,12 +2,22 @@ ## New APIs +- `IntegerLiteralExprSyntax` and `FloatLiteralExprSyntax` now have a computed `representedLiteralValue` property. + - Description: Allows retrieving the represented literal value when valid. + - Issue: https://github.com/apple/swift-syntax/issues/405 + - Pull Request: https://github.com/apple/swift-syntax/pull/2605 + ## API Behavior Changes ## Deprecations ## API-Incompatible Changes +- Moved `Radix` and `IntegerLiteralExprSyntax.radix` from `SwiftRefactor` to `SwiftSyntax`. + - Description: Allows retrieving the radix value from the `literal.text`. + - Issue: https://github.com/apple/swift-syntax/issues/405 + - Pull Request: https://github.com/apple/swift-syntax/pull/2605 + ## Template - *Affected API or two word description* diff --git a/Sources/SwiftRefactor/IntegerLiteralUtilities.swift b/Sources/SwiftRefactor/IntegerLiteralUtilities.swift index 3b668fcf85b..8129b02ff56 100644 --- a/Sources/SwiftRefactor/IntegerLiteralUtilities.swift +++ b/Sources/SwiftRefactor/IntegerLiteralUtilities.swift @@ -17,46 +17,6 @@ import SwiftSyntax #endif extension IntegerLiteralExprSyntax { - public enum Radix { - case binary - case octal - case decimal - case hex - - public var size: Int { - switch self { - case .binary: return 2 - case .octal: return 8 - case .decimal: return 10 - case .hex: return 16 - } - } - - /// The prefix that is used to express an integer literal with this - /// radix in Swift source code, e.g., "0x" for hexadecimal. - public var literalPrefix: String { - switch self { - case .binary: return "0b" - case .octal: return "0o" - case .hex: return "0x" - case .decimal: return "" - } - } - } - - public var radix: Radix { - let text = self.literal.text - if text.starts(with: "0b") { - return .binary - } else if text.starts(with: "0o") { - return .octal - } else if text.starts(with: "0x") { - return .hex - } else { - return .decimal - } - } - /// Returns an (arbitrarily) "ideal" number of digits that should constitute /// a separator-delimited "group" in an integer literal. var idealGroupSize: Int { diff --git a/Sources/SwiftSyntax/Convenience.swift b/Sources/SwiftSyntax/Convenience.swift index 6521bd0e569..0c9b74d66e4 100644 --- a/Sources/SwiftSyntax/Convenience.swift +++ b/Sources/SwiftSyntax/Convenience.swift @@ -68,6 +68,90 @@ extension EnumCaseParameterSyntax { } } +extension FloatLiteralExprSyntax { + /// A computed property representing the floating-point value parsed from the associated `literal.text` property. + /// + /// - Returns: A double value parsed from the associated`literal.text`, or `nil` if the text cannot be parsed as a double. + public var representedLiteralValue: Double? { + guard !hasError else { return nil } + + let floatingDigitsWithoutUnderscores = literal.text.filter { + $0 != "_" + } + + return Double(floatingDigitsWithoutUnderscores) + } +} + +extension IntegerLiteralExprSyntax { + public enum Radix { + case binary + case octal + case decimal + case hex + + public var size: Int { + switch self { + case .binary: return 2 + case .octal: return 8 + case .decimal: return 10 + case .hex: return 16 + } + } + + /// The prefix that is used to express an integer literal with this + /// radix in Swift source code, e.g., "0x" for hexadecimal. + public var literalPrefix: String { + switch self { + case .binary: return "0b" + case .octal: return "0o" + case .hex: return "0x" + case .decimal: return "" + } + } + + fileprivate var offset: Int { + switch self { + case .binary, .hex, .octal: + return 2 + case .decimal: + return 0 + } + } + } + + public var radix: Radix { + let text = self.literal.text + if text.starts(with: "0b") { + return .binary + } else if text.starts(with: "0o") { + return .octal + } else if text.starts(with: "0x") { + return .hex + } else { + return .decimal + } + } + + /// A computed property representing the integer value parsed from the associated `literal.text` property, considering the specified radix. + /// + /// - Returns: An integer value parsed from the associated `literal.text`, or `nil` if the text cannot be parsed as an integer. + public var representedLiteralValue: Int? { + guard !hasError else { return nil } + + let text = literal.text + let radix = self.radix + let digitsStartIndex = text.index(text.startIndex, offsetBy: radix.offset) + let textWithoutPrefix = text.suffix(from: digitsStartIndex) + + let textWithoutPrefixOrUnderscores = textWithoutPrefix.filter { + $0 != "_" + } + + return Int(textWithoutPrefixOrUnderscores, radix: radix.size) + } +} + extension MemberAccessExprSyntax { /// Creates a new ``MemberAccessExprSyntax`` where the accessed member is represented by /// an identifier without specifying argument labels. diff --git a/Tests/SwiftSyntaxTest/SyntaxTests.swift b/Tests/SwiftSyntaxTest/SyntaxTests.swift index 30055ef9817..a07a5265c19 100644 --- a/Tests/SwiftSyntaxTest/SyntaxTests.swift +++ b/Tests/SwiftSyntaxTest/SyntaxTests.swift @@ -168,4 +168,55 @@ class SyntaxTests: XCTestCase { XCTAssertEqual(funcKeywordInTree2?.as(TokenSyntax.self)?.tokenKind, .keyword(.func)) XCTAssertNotEqual(funcKeywordInTree1.id, funcKeywordInTree2?.id) } + + func testIntegerLiteralExprSyntax() { + let testCases: [UInt: (String, Int?)] = [ + #line: ("2", 2), + #line: ("02", 2), + #line: ("020", 20), + #line: ("-02", -2), + #line: ("2_00_0000", 2_00_0000), + #line: ("-2_00_0000", -2_00_0000), + #line: ("foo", nil), + #line: ("999999999999999999999999999999999999999999999999999999999999999999999999999999", nil), + #line: ("0b1010101", 85), + #line: ("0xFF", 255), + #line: ("0o777", 511), + #line: ("0b001100", 0b001100), + #line: ("4_2", 4_2), + #line: ("0o3434", 0o3434), + #line: ("0xba11aD", 0xba11aD), + #line: ("🐋", nil), + #line: ("-0xA", nil), + #line: ("-0o7", nil), + #line: ("-0b1", nil), + ] + + for (line, testCase) in testCases { + let (value, expected) = testCase + let expr = IntegerLiteralExprSyntax(literal: .integerLiteral(value)) + XCTAssertEqual(expr.representedLiteralValue, expected, line: line) + } + } + + func testFloatLiteralExprSyntax() { + let testCases: [UInt: (String, Double?)] = [ + #line: ("2", 2), + #line: ("2_00_00.001", 2_00_00.001), + #line: ("5.3_8", 5.3_8), + #line: ("12e3", 12000.0), + #line: ("32E1", 320.0), + #line: ("0xdEFACE.C0FFEEp+1", 0xdEFACE.C0FFEEp+1), + #line: ("0xaffab1e.e1fP-2", 0xaffab1e.e1fP-2), + #line: ("🥥", nil), + #line: ("12e+3", 12000.0), + #line: ("12e-3", 0.012), + ] + + for (line, testCase) in testCases { + let (value, expected) = testCase + let expr = FloatLiteralExprSyntax(literal: .floatLiteral(value)) + XCTAssertEqual(expr.representedLiteralValue, expected, line: line) + } + } }