From fd85633230dc925bb4651f2a8094ff237d73ca1b Mon Sep 17 00:00:00 2001 From: Padmashree S S Date: Thu, 16 Apr 2026 02:02:14 +0530 Subject: [PATCH] Add function call inlining --- .../SwiftRefactor/InlineFunctionCall.swift | 92 ++++++ .../InlineFunctionCallTests.swift | 262 ++++++++++++++++++ 2 files changed, 354 insertions(+) create mode 100644 Sources/SwiftRefactor/InlineFunctionCall.swift create mode 100644 Tests/SwiftRefactorTest/InlineFunctionCallTests.swift diff --git a/Sources/SwiftRefactor/InlineFunctionCall.swift b/Sources/SwiftRefactor/InlineFunctionCall.swift new file mode 100644 index 00000000000..5a170889c4a --- /dev/null +++ b/Sources/SwiftRefactor/InlineFunctionCall.swift @@ -0,0 +1,92 @@ +//===----------------------------------------------------------------------===// +// +// 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 + +public struct InlineFunctionCall: SyntaxRefactoringProvider { + + public typealias Input = FunctionCallExprSyntax + public typealias Output = Syntax + public typealias Context = Void + + public static func refactor(syntax: FunctionCallExprSyntax, in context: Void) throws -> Syntax { + + guard let calledExpr = syntax.calledExpression.as(DeclReferenceExprSyntax.self) else { + throw RefactoringNotApplicableError("cursor must be on a function call") + } + let funcName = calledExpr.baseName.text + + // Find declaration + guard let root = syntax.root.as(SourceFileSyntax.self), + let declaration = root.statements.compactMap({ $0.item.as(FunctionDeclSyntax.self) }) + .first(where: { $0.name.text == funcName }) + else { + throw RefactoringNotApplicableError("could not find function definition") + } + + // Extract body of function + guard let body = declaration.body else { + throw RefactoringNotApplicableError("function has no body to inline") + } + + // Substitution logic + var substitutionMap: [String: ExprSyntax] = [:] + let parameters = declaration.signature.parameterClause.parameters + let arguments = syntax.arguments + + for (param, arg) in zip(parameters, arguments) { + let paramName = param.secondName?.text ?? param.firstName.text + substitutionMap[paramName] = arg.expression + } + + let rewriter = ParameterSubstitutionRewriter(map: substitutionMap) + + if body.statements.count == 1, + let returnStmt = body.statements.first?.item.as(ReturnStmtSyntax.self), + let expr = returnStmt.expression + { + + let newExpr = rewriter.visit(expr) + return Syntax(newExpr) + } + + let newBody = rewriter.visit(body) + + let closure = ClosureExprSyntax { + newBody.statements + } + + let call = FunctionCallExprSyntax( + calledExpression: ExprSyntax(closure), + leftParen: .leftParenToken(), + arguments: LabeledExprListSyntax([]), + rightParen: .rightParenToken() + ) + + return Syntax(call) + } +} + +private class ParameterSubstitutionRewriter: SyntaxRewriter { + let map: [String: ExprSyntax] + init(map: [String: ExprSyntax]) { self.map = map } + + override func visit(_ node: DeclReferenceExprSyntax) -> ExprSyntax { + if let replacement = map[node.baseName.text] { + return + replacement + .with(\.leadingTrivia, node.leadingTrivia) + .with(\.trailingTrivia, node.trailingTrivia) + } + return super.visit(node) + } +} diff --git a/Tests/SwiftRefactorTest/InlineFunctionCallTests.swift b/Tests/SwiftRefactorTest/InlineFunctionCallTests.swift new file mode 100644 index 00000000000..2523352eb8f --- /dev/null +++ b/Tests/SwiftRefactorTest/InlineFunctionCallTests.swift @@ -0,0 +1,262 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2023 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 SwiftParser +import SwiftRefactor +import SwiftSyntax +import SwiftSyntaxBuilder +import XCTest +import _SwiftSyntaxTestSupport + +final class InlineFunctionCallTests: XCTestCase { + private func functionCallExpr(from markedText: String) throws -> FunctionCallExprSyntax { + let (markers, source) = extractMarkers(markedText) + let startOffset = markers["1️⃣"] ?? markers["DEFAULT"] ?? 0 + + class FunctionCallFinder: SyntaxAnyVisitor { + let startOffset: Int + var found: FunctionCallExprSyntax? + + init(startOffset: Int) { + self.startOffset = startOffset + super.init(viewMode: .all) + } + + override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind { + if found != nil || node.endPosition.utf8Offset < startOffset { + return .skipChildren + } + + if node.positionAfterSkippingLeadingTrivia.utf8Offset >= startOffset, + let call = node.as(FunctionCallExprSyntax.self) + { + found = call + return .skipChildren + } + + return .visitChildren + } + } + + let finder = FunctionCallFinder(startOffset: startOffset) + finder.walk(Syntax(Parser.parse(source: source))) + return try XCTUnwrap( + finder.found, + "Could not find function call expression after marker", + file: #file, + line: #line + ) + } + + // MARK: - Basic Expression Inlining + + func testInlineSimpleFunctionExpression() throws { + let input = try functionCallExpr( + from: """ + func double(_ x: Int) -> Int { + return x * 2 + } + + func example() { + let result = 1️⃣double(5) + } + """ + ) + + let expected: ExprSyntax = "5 * 2" + + try assertRefactor( + input, + context: (), + provider: InlineFunctionCall.self, + expected: expected + ) + } + + func testInlineMultipleParametersExpression() throws { + let input = try functionCallExpr( + from: """ + func add(_ a: Int, _ b: Int) -> Int { + return a + b + } + + func example() { + let result = 1️⃣add(10, 20) + } + """ + ) + + let expected: ExprSyntax = "10 + 20" + + try assertRefactor( + input, + context: (), + provider: InlineFunctionCall.self, + expected: expected + ) + } + + // MARK: - Closure Wrapping Cases + + func testInlineMultiStatementFunction() throws { + let input = try functionCallExpr( + from: """ + func compute(_ x: Int) -> Int { + let y = x + 1 + return y + } + + func example() { + let result = 1️⃣compute(5) + } + """ + ) + + let result = try InlineFunctionCall.refactor(syntax: input, in: ()) + + XCTAssertTrue(result.description.contains("let y = 5 + 1")) + XCTAssertTrue(result.description.contains("return y")) + XCTAssertTrue(result.description.contains("()")) + } + + func testInlineVoidFunction() throws { + let input = try functionCallExpr( + from: """ + func log(_ x: Int) { + print(x) + } + + func example() { + 1️⃣log(5) + } + """ + ) + + let result = try InlineFunctionCall.refactor(syntax: input, in: ()) + + XCTAssertTrue(result.description.contains("print(5)")) + XCTAssertTrue(result.description.contains("()")) + } + + // MARK: - Edge Cases + + func testInlineExpressionBodyFunction() throws { + let input = try functionCallExpr( + from: """ + func double(_ x: Int) -> Int { x * 2 } + + func example() { + let result = 1️⃣double(5) + } + """ + ) + + let result = try InlineFunctionCall.refactor(syntax: input, in: ()) + + XCTAssertTrue(result.description.contains("5 * 2")) + } + + func testParameterUsedMultipleTimes() throws { + let input = try functionCallExpr( + from: """ + func square(_ x: Int) -> Int { + return x * x + } + + func example() { + let result = 1️⃣square(5) + } + """ + ) + + let result = try InlineFunctionCall.refactor(syntax: input, in: ()) + let output = result.description + + let count = output.components(separatedBy: "5").count - 1 + XCTAssertGreaterThanOrEqual(count, 2) + } + + func testFunctionDefinedAfterCall() throws { + let input = try functionCallExpr( + from: """ + func example() { + let result = 1️⃣increment(5) + } + + func increment(_ x: Int) -> Int { + return x + 1 + } + """ + ) + + let expected: ExprSyntax = "5 + 1" + + try assertRefactor( + input, + context: (), + provider: InlineFunctionCall.self, + expected: expected + ) + } + + // MARK: - Tests to log error + + func testFunctionNotFound() throws { + let input = try functionCallExpr( + from: """ + func example() { + let result = 1️⃣unknown(5) + } + """ + ) + + XCTAssertThrowsError( + try InlineFunctionCall.refactor(syntax: input, in: ()) + ) + } + + func testMethodCallShouldFail() throws { + let input = try functionCallExpr( + from: """ + struct A { + func foo() {} + } + + let a = A() + 1️⃣a.foo() + """ + ) + + XCTAssertThrowsError( + try InlineFunctionCall.refactor(syntax: input, in: ()) + ) + } + + func testFewerArgumentsThanParameters() throws { + let input = try functionCallExpr( + from: """ + func add(_ a: Int, _ b: Int) -> Int { + return a + b + } + + func example() { + let result = 1️⃣add(5) + } + """ + ) + + let result = try InlineFunctionCall.refactor(syntax: input, in: ()) + + XCTAssertTrue(result.description.contains("5")) + XCTAssertTrue(result.description.contains("b")) + } +}