diff --git a/Release Notes/601.md b/Release Notes/601.md index eee0be858b2..b7e30fc4553 100644 --- a/Release Notes/601.md +++ b/Release Notes/601.md @@ -2,6 +2,11 @@ ## New APIs +- `FunctionDecl`, `InitializerDecl`, `SubscriptDecl`, `EnumCaseElement`, and `MacroDecl` gained a new computed property: `symbolName` + - Description: The `symbolName` property provides a string representation of the declaration's name along with its parameter labels. For example, a function `func greet(name: String)` will have the symbol name `greet(name:)`. + - Issue: https://github.com/apple/swift-syntax/issues/2488 + - Pull Request: https://github.com/apple/swift-syntax/pull/2583 + ## API Behavior Changes ## Deprecations diff --git a/Sources/SwiftSyntax/SymbolName.swift b/Sources/SwiftSyntax/SymbolName.swift new file mode 100644 index 00000000000..ee595c36a95 --- /dev/null +++ b/Sources/SwiftSyntax/SymbolName.swift @@ -0,0 +1,139 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +extension FunctionDeclSyntax { + /// The symbol name of the function declaration. + /// + /// The symbol name is a string representation of the function name along with its parameter labels. + /// For example, a function `func greet(name: String)` will have the symbol name `greet(name:)`. + public var symbolName: String { + SwiftSyntax.symbolName( + fromBaseName: name.text, + parameters: signature.parameterClause + ) + } +} + +extension InitializerDeclSyntax { + /// The symbol name of the initializer declaration. + /// + /// The symbol name is a string representation of the initializer along with its parameter labels. + /// For example, an initializer `init(name: String)` will have the symbol name `init(name:)`. + public var symbolName: String { + SwiftSyntax.symbolName( + fromBaseName: initKeyword.text, + parameters: signature.parameterClause + ) + } +} + +extension SubscriptDeclSyntax { + /// The symbol name of the subscript declaration. + /// + /// The symbol name is a string representation of the subscript along with its parameter labels. + /// For example, a subscript `subscript(index: Int)` will have the symbol name `subscript(_:)`. + public var symbolName: String { + SwiftSyntax.symbolName( + fromBaseName: subscriptKeyword.text, + parameters: parameterClause, + isForSubscript: true + ) + } +} + +extension EnumCaseElementSyntax { + /// The symbol name of the enum case element. + /// + /// The symbol name is a string representation of the enum case name along with its associated value labels (if any). + /// For example, an enum case `case foo(bar: Int)` will have the symbol name `foo(bar:)`. + public var symbolName: String { + let caseName = name.text + + guard let associatedValues = parameterClause else { + return caseName + } + + let argumentLabels = associatedValues.parameters + .map { parameter in + guard let firstName = parameter.firstName else { + return "_:" + } + + return firstName.text + ":" + } + .joined() + + return "\(caseName)(\(argumentLabels))" + } +} + +extension MacroDeclSyntax { + /// The symbol name of the macro declaration. + /// + /// The symbol name is a string representation of the macro name along with its parameter labels. + /// For example, a macro `macro greet(name: String)` will have the symbol name `greet(name:)`. + public var symbolName: String { + SwiftSyntax.symbolName( + fromBaseName: name.text, + parameters: signature.parameterClause + ) + } +} + +/// Generates the symbol name by combining the base name and parameter labels. +/// +/// If the parameter has two names (e.g., `external internal: Int`), the first name is considered the argument label. +/// If the parameter has only one name and it's not a subscript parameter, it is considered the argument label. +/// +/// - Parameters: +/// - baseName: The base name of the symbol (e.g., function name, initializer, subscript). +/// - parameters: The function parameter clause containing the parameter labels. +/// - Returns: The symbol name with the base name and parameter labels combined. +private func symbolName( + fromBaseName baseName: String, + parameters: FunctionParameterClauseSyntax, + isForSubscript: Bool = false +) -> String { + let argumentLabels = parameters.parameters + .map { parameter in + let argumentLabelText = parameter.argumentName(isForSubscript: isForSubscript) ?? "_" + return argumentLabelText + ":" + } + .joined() + + return "\(baseName)(\(argumentLabels))" +} + +extension FunctionParameterSyntax { + /// The argument name (label) of the function parameter. + fileprivate func argumentName(isForSubscript: Bool = false) -> String? { + // If we have two names, the first one is the argument label + if secondName != nil { + return firstName.asIdentifierToken + } + + if isForSubscript { + return nil + } + + return firstName.asIdentifierToken + } +} + +extension TokenSyntax { + fileprivate var asIdentifierToken: String? { + switch tokenKind { + case .identifier: return self.text + default: return nil + } + } +} diff --git a/Tests/SwiftSyntaxTest/SymbolNameTests.swift b/Tests/SwiftSyntaxTest/SymbolNameTests.swift new file mode 100644 index 00000000000..b29e0a6073e --- /dev/null +++ b/Tests/SwiftSyntaxTest/SymbolNameTests.swift @@ -0,0 +1,326 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +import SwiftSyntax +import XCTest + +final class SymbolNameTests: XCTestCase { + + // MARK: - Functions + + func testFunctionNameWithoutParameters() { + assertSymbolName(ofFunction: "func name() {}", expecting: "name()") + } + + func testFunctionNameWithSingleNamedParameter() { + assertSymbolName(ofFunction: "func name(one: Int) {}", expecting: "name(one:)") + } + + func testFunctionNameWithSingleUnnamedParameter() { + assertSymbolName(ofFunction: "func name(_: Int) {}", expecting: "name(_:)") + } + + func testFunctionNameWithExternalParameterName() { + assertSymbolName(ofFunction: "func name(one two: Int) {}", expecting: "name(one:)") + } + + func testFunctionNameWithExplicitExternalParameterName() { + assertSymbolName(ofFunction: "func name(one _: Int) {}", expecting: "name(one:)") + } + + func testFunctionNameWithImplicitExternalParameterName() { + assertSymbolName(ofFunction: "func name(_ two: Int) {}", expecting: "name(_:)") + } + + func testFunctionNameWithOnlyAnonymousParameters() { + assertSymbolName(ofFunction: "func name(_ _: Int) {}", expecting: "name(_:)") + } + + func testFunctionNameWithMultipleNamedParameters() { + assertSymbolName(ofFunction: "func name(one two: Int, three: Int) {}", expecting: "name(one:three:)") + } + + func testFunctionNameWithAllParametersHavingExternalNames() { + assertSymbolName(ofFunction: "func name(one two: Int, three four: Int) {}", expecting: "name(one:three:)") + } + + func testFunctionNameWithMixedNamedAndAnonymousParameters() { + assertSymbolName(ofFunction: "func name(one two: Int, _ four: Int) {}", expecting: "name(one:_:)") + } + + func testFunctionNameWithAllAnonymousParameters() { + assertSymbolName(ofFunction: "func name(_ two: Int, _ four: Int) {}", expecting: "name(_:_:)") + } + + func testFunctionNameWithBackticks() { + assertSymbolName(ofFunction: "func `name`() {}", expecting: "`name`()") + } + + func testFunctionNameWithParameterNameInBackticks() { + assertSymbolName(ofFunction: "func name(`one`: Int) {}", expecting: "name(`one`:)") + } + + func testOperatorFunctionName() { + assertSymbolName(ofFunction: "func == (one: Int, two: Int)", expecting: "==(one:two:)") + } + + // MARK: - Initializer + + func testInitializerWithoutParameters() { + assertSymbolName(ofInitializer: "init() {}", expecting: "init()") + } + + func testInitializerWithSingleNamedParameter() { + assertSymbolName(ofInitializer: "init(one: Int) {}", expecting: "init(one:)") + } + + func testInitializerWithSingleUnnamedParameter() { + assertSymbolName(ofInitializer: "init(_: Int) {}", expecting: "init(_:)") + } + + func testInitializerWithExternalParameterName() { + assertSymbolName(ofInitializer: "init(one two: Int) {}", expecting: "init(one:)") + } + + func testInitializerWithExplicitExternalParameterName() { + assertSymbolName(ofInitializer: "init(one _: Int) {}", expecting: "init(one:)") + } + + func testInitializerWithImplicitExternalParameterName() { + assertSymbolName(ofInitializer: "init(_ two: Int) {}", expecting: "init(_:)") + } + + func testInitializerWithOnlyAnonymousParameters() { + assertSymbolName(ofInitializer: "init(_ _: Int) {}", expecting: "init(_:)") + } + + func testInitializerWithMultipleNamedParameters() { + assertSymbolName(ofInitializer: "init(one two: Int, three: Int) {}", expecting: "init(one:three:)") + } + + func testInitializerWithAllParametersHavingExternalNames() { + assertSymbolName(ofInitializer: "init(one two: Int, three four: Int) {}", expecting: "init(one:three:)") + } + + func testInitializerWithMixedNamedAndAnonymousParameters() { + assertSymbolName(ofInitializer: "init(one two: Int, _ four: Int) {}", expecting: "init(one:_:)") + } + + func testInitializerWithAllAnonymousParameters() { + assertSymbolName(ofInitializer: "init(_ two: Int, _ four: Int) {}", expecting: "init(_:_:)") + } + + func testInitializerWithNameInBackticks() { + assertSymbolName(ofInitializer: "init(`one`: Int) {}", expecting: "init(`one`:)") + } + + // MARK: - Subscript + + func testSubscriptNameWithoutParameters() { + assertSymbolName(ofSubscript: "subscript() -> Int { 0 }", expecting: "subscript()") + } + + func testSubscriptNameWithSingleNamedParameter() { + assertSymbolName(ofSubscript: "subscript(index: Int) -> Int { 0 }", expecting: "subscript(_:)") + } + + func testSubscriptNameWithSingleUnnamedParameter() { + assertSymbolName(ofSubscript: "subscript(_: Int) -> Int { 0 }", expecting: "subscript(_:)") + } + + func testSubscriptNameWithExternalParameterName() { + assertSymbolName(ofSubscript: "subscript(index i: Int) -> Int { 0 }", expecting: "subscript(index:)") + } + + func testSubscriptNameWithExplicitExternalParameterName() { + assertSymbolName(ofSubscript: "subscript(index _: Int) -> Int { 0 }", expecting: "subscript(index:)") + } + + func testSubscriptNameWithImplicitExternalParameterName() { + assertSymbolName(ofSubscript: "subscript(_ i: Int) -> Int { 0 }", expecting: "subscript(_:)") + } + + func testSubscriptNameWithOnlyAnonymousParameters() { + assertSymbolName(ofSubscript: "subscript(_ _: Int) -> Int { 0 }", expecting: "subscript(_:)") + } + + func testSubscriptNameWithMultipleNamedParameters() { + assertSymbolName(ofSubscript: "subscript(x: Int, y: Int) -> Int { 0 }", expecting: "subscript(_:_:)") + } + + func testSubscriptNameWithMultipleParametersAndExternalNames() { + assertSymbolName( + ofSubscript: "subscript(indexX x: Int, indexY y: Int) -> Int { 0 }", + expecting: "subscript(indexX:indexY:)" + ) + } + + func testSubscriptNameWithParameterNameInBackticks() { + assertSymbolName(ofSubscript: "subscript(`index` i: Int) -> Int { 0 }", expecting: "subscript(`index`:)") + } + + // MARK: - Enum Case Element + + func testEnumCaseElementWithoutAssociatedValues() { + assertSymbolName(ofEnumCase: "case a", expecting: "a") + } + + func testEnumCaseElementWithSingleNamedAssociatedValue() { + assertSymbolName(ofEnumCase: "case b(c: Int)", expecting: "b(c:)") + } + + func testEnumCaseElementWithSingleUnnamedAssociatedValue() { + assertSymbolName(ofEnumCase: "case d(Int)", expecting: "d(_:)") + } + + func testEnumCaseElementWithSingleUnnamedAssociatedValueWithUnderscore() { + assertSymbolName(ofEnumCase: "case e(_: Int)", expecting: "e(_:)") + } + + func testEnumCaseElementWithMultipleNamedAssociatedValues() { + assertSymbolName(ofEnumCase: "case f(g: Int, h: Int)", expecting: "f(g:h:)") + } + + func testEnumCaseElementNameWithBackticks() { + assertSymbolName(ofEnumCase: "case `i`", expecting: "`i`") + } + + func testEnumCaseElementWithAssociatedValueNameInBackticks() { + assertSymbolName(ofEnumCase: "case j(`k`: Int)", expecting: "j(`k`:)") + } + + // MARK: - Macro + + func testMacroDeclWithoutParameters() { + assertSymbolName(ofMacroDecl: "macro myMacro()", expecting: "myMacro()") + } + + func testMacroDeclWithSingleNamedParameter() { + assertSymbolName(ofMacroDecl: "macro myMacro(x: Int)", expecting: "myMacro(x:)") + } + + func testMacroDeclWithSingleUnnamedParameter() { + assertSymbolName(ofMacroDecl: "macro myMacro(_: Int)", expecting: "myMacro(_:)") + } + + func testMacroDeclWithExternalParameterName() { + assertSymbolName(ofMacroDecl: "macro myMacro(external internal: Int)", expecting: "myMacro(external:)") + } + + func testMacroDeclWithExplicitExternalParameterName() { + assertSymbolName(ofMacroDecl: "macro myMacro(external _: Int)", expecting: "myMacro(external:)") + } + + func testMacroDeclWithImplicitExternalParameterName() { + assertSymbolName(ofMacroDecl: "macro myMacro(_ internal: Int)", expecting: "myMacro(_:)") + } + + func testMacroDeclWithMultipleNamedParameters() { + assertSymbolName(ofMacroDecl: "macro myMacro(x: Int, y: String)", expecting: "myMacro(x:y:)") + } + + func testMacroDeclWithAllParametersHavingExternalNames() { + assertSymbolName( + ofMacroDecl: "macro myMacro(external1 internal1: Int, external2 internal2: String)", + expecting: "myMacro(external1:external2:)" + ) + } + + func testMacroDeclWithMixedNamedAndUnnamedParameters() { + assertSymbolName(ofMacroDecl: "macro myMacro(x: Int, _: String)", expecting: "myMacro(x:_:)") + } + + func testMacroDeclWithNameInBackticks() { + assertSymbolName(ofMacroDecl: "macro `myMacro`()", expecting: "`myMacro`()") + } + + func testMacroDeclWithParameterNameInBackticks() { + assertSymbolName(ofMacroDecl: "macro myMacro(`x`: Int)", expecting: "myMacro(`x`:)") + } + + func testMacroDeclWithExternalParameterNameInBackticks() { + assertSymbolName(ofMacroDecl: "macro myMacro(`external` internal: Int)", expecting: "myMacro(`external`:)") + } +} + +// MARK: - Assertions + +private func assertSymbolName( + ofFunction function: DeclSyntax, + expecting expectedSymbolName: String, + file: StaticString = #filePath, + line: UInt = #line +) { + guard let functionDecl = function.as(FunctionDeclSyntax.self) else { + XCTFail("Expected function declaration not found.", file: file, line: line) + return + } + + XCTAssertEqual(functionDecl.symbolName, expectedSymbolName, file: file, line: line) +} + +private func assertSymbolName( + ofInitializer initializer: DeclSyntax, + expecting expectedSymbolName: String, + file: StaticString = #filePath, + line: UInt = #line +) { + guard let initializerDecl = initializer.as(InitializerDeclSyntax.self) else { + XCTFail("Expected initializer declaration not found.", file: file, line: line) + return + } + + XCTAssertEqual(initializerDecl.symbolName, expectedSymbolName, file: file, line: line) +} + +private func assertSymbolName( + ofSubscript subscriptDeclaration: DeclSyntax, + expecting expectedSymbolName: String, + file: StaticString = #filePath, + line: UInt = #line +) { + guard let subscriptDecl = subscriptDeclaration.as(SubscriptDeclSyntax.self) else { + XCTFail("Expected subscript declaration not found.", file: file, line: line) + return + } + + XCTAssertEqual(subscriptDecl.symbolName, expectedSymbolName, file: file, line: line) +} + +private func assertSymbolName( + ofEnumCase enumCaseDeclaration: DeclSyntax, + expecting expectedSymbolName: String, + file: StaticString = #filePath, + line: UInt = #line +) { + guard let enumCaseDecl = enumCaseDeclaration.as(EnumCaseDeclSyntax.self), + let enumCaseElement = enumCaseDecl.elements.first + else { + XCTFail("Expected enum case element declaration not found.", file: file, line: line) + return + } + XCTAssertEqual(enumCaseElement.symbolName, expectedSymbolName, file: file, line: line) +} + +private func assertSymbolName( + ofMacroDecl macroDecl: DeclSyntax, + expecting expectedSymbolName: String, + file: StaticString = #filePath, + line: UInt = #line +) { + guard let macroDeclSyntax = macroDecl.as(MacroDeclSyntax.self) else { + XCTFail("Expected macro declaration not found.", file: file, line: line) + return + } + + XCTAssertEqual(macroDeclSyntax.symbolName, expectedSymbolName, file: file, line: line) +}