Repository: swiftlang/swift-syntax (swift)

Issue:
[SR-13584] Make convenience methods returning an the value of an integer literal as Int and of floating literal as Float return an optional

   |                  |                 |
   |------------------|-----------------|
   |Previous ID       | SR-13584      |
   |Radar             | None         |
   |Original Reporter | vermont42 (JIRA User)      |
   |Type              | Bug    |

   <details>
  <summary>Additional Detail from JIRA</summary>

   |                  |                 |
   |------------------|-----------------|
   |Votes             | 0         |
   |Component/s       | SwiftSyntax    |
   |Labels            | Bug        |
   |Assignee          | vermont42 (JIRA)      |
   |Priority          | Medium      |

   

   md5: 14ad515b7c276d59a1dd762b567c3a4b

  </details>





**Issue Description:**


After the [PR](https://github.com/apple/swift-syntax/pull/239) for SR-11580 merged, there was consensus, based on feedback, that the new convenience methods `floatingValue` and `integerValue`, should be optional in order to prevent potential crashes. This story is for implementation of this change. There was additional feedback involving naming and genericity. Depending on PR-review feedback, this story may encompass implementation of these changes as well.

Pull request description:
Resolves https://github.com/apple/swift-syntax/issues/405

Patch:
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.


Test patch:
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)
+    }
+  }
 }