mirror of
https://github.com/nicklockwood/SwiftFormat.git
synced 2026-05-17 10:30:35 +00:00
4876d0a888
Co-authored-by: calda <1811727+calda@users.noreply.github.com>
402 lines
17 KiB
Swift
402 lines
17 KiB
Swift
//
|
|
// UnusedArguments.swift
|
|
// SwiftFormat
|
|
//
|
|
// Created by Nick Lockwood on 1/3/17.
|
|
// Copyright © 2024 Nick Lockwood. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
public extension FormatRule {
|
|
/// Replace unused arguments with an underscore
|
|
static let unusedArguments = FormatRule(
|
|
help: "Mark unused function arguments with `_`.",
|
|
options: ["strip-unused-args"]
|
|
) { formatter in
|
|
guard !formatter.options.fragment else { return }
|
|
|
|
// Function arguments
|
|
formatter.forEach(.keyword) { i, token in
|
|
guard formatter.options.stripUnusedArguments != .closureOnly,
|
|
["func", "init", "subscript"].contains(token.string)
|
|
else { return }
|
|
|
|
// In subscripts and operators, external function labels are unnecessary
|
|
let isOperator = (token.string == "subscript") ||
|
|
(token.string == "func" && formatter.next(.nonSpaceOrCommentOrLinebreak, after: i)?.isOperator == true)
|
|
|
|
guard let declaration = formatter.parseFunctionDeclaration(keywordIndex: i),
|
|
let bodyRange = declaration.bodyRange
|
|
else { return }
|
|
|
|
var arguments = declaration.arguments.filter { $0.internalLabel != nil }
|
|
var argNames = arguments.compactMap(\.internalLabel)
|
|
|
|
formatter.removeUsed(from: &argNames, with: &arguments, in: bodyRange.lowerBound + 1 ..< bodyRange.upperBound)
|
|
for argument in arguments.reversed() {
|
|
// In subscripts and operators, external function labels are unnecessary
|
|
if isOperator {
|
|
// Convert `_ name:` to just `_:`
|
|
if let externalLabelIndex = argument.externalLabelIndex, argument.externalLabel == nil {
|
|
formatter.removeTokens(in: (externalLabelIndex + 1) ... argument.internalLabelIndex)
|
|
}
|
|
|
|
// Convert `name:` to just `_:`
|
|
else {
|
|
formatter.replaceToken(at: argument.internalLabelIndex, with: .identifier("_"))
|
|
}
|
|
}
|
|
|
|
// When using --stripunusedargs unnamed-only, only remove the internal label
|
|
// when the external label is already explicitly removed.
|
|
else if formatter.options.stripUnusedArguments == .unnamedOnly {
|
|
// Convert `_ name:` to just `_:`
|
|
if let externalLabelIndex = argument.externalLabelIndex, argument.externalLabel == nil {
|
|
formatter.removeTokens(in: (externalLabelIndex + 1) ... argument.internalLabelIndex)
|
|
}
|
|
}
|
|
|
|
else {
|
|
// Convert `_ name:` to just `_:`
|
|
if let externalLabelIndex = argument.externalLabelIndex, argument.externalLabel == nil {
|
|
formatter.removeTokens(in: (externalLabelIndex + 1) ... argument.internalLabelIndex)
|
|
}
|
|
|
|
// Convert `name:` to `name _:`,
|
|
else if argument.externalLabelIndex == nil, !isOperator {
|
|
formatter.insert([.space(" "), .identifier("_")], at: argument.internalLabelIndex + 1)
|
|
}
|
|
|
|
// Convert `in name:` to `in _:`
|
|
else {
|
|
formatter.replaceToken(at: argument.internalLabelIndex, with: .identifier("_"))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// For loop variables (only when --strip-unused-args is "always")
|
|
if formatter.options.stripUnusedArguments == .all {
|
|
formatter.forEach(.keyword("for")) { i, _ in
|
|
// Find the "in" keyword that belongs to this for loop
|
|
guard let inIndex = formatter.index(of: .keyword("in"), after: i) else { return }
|
|
|
|
// Collect binding names between "for" and "in"
|
|
var argNames = [String]()
|
|
var nameIndexes = [Int]()
|
|
var index = i + 1
|
|
while index < inIndex {
|
|
switch formatter.tokens[index] {
|
|
case .keyword("case"):
|
|
// Skip `for case .foo in ...` pattern-matching for loops
|
|
return
|
|
case .identifier("await"), .keyword("await"):
|
|
// Skip `for await ...` async sequence iteration marker
|
|
break
|
|
case .identifier:
|
|
let name = formatter.tokens[index].unescaped()
|
|
guard name != "_" else { break }
|
|
argNames.append(name)
|
|
nameIndexes.append(index)
|
|
case .delimiter(":"):
|
|
// Skip type annotation after `:` (e.g. `for x: CGFloat? in`)
|
|
if let typeStart = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: index),
|
|
let type = formatter.parseType(at: typeStart)
|
|
{
|
|
index = type.range.upperBound
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
index += 1
|
|
}
|
|
|
|
guard !argNames.isEmpty else { return }
|
|
|
|
// Find the loop body
|
|
guard let bodyStart = formatter.index(of: .startOfScope("{"), after: inIndex),
|
|
let bodyEnd = formatter.endOfScope(at: bodyStart) else { return }
|
|
|
|
// Check usage in the body
|
|
formatter.removeUsed(from: &argNames, with: &nameIndexes, in: bodyStart + 1 ..< bodyEnd)
|
|
|
|
// Check usage in the `where` clause (if present)
|
|
if let whereIndex = formatter.index(of: .keyword("where"), after: inIndex),
|
|
whereIndex < bodyStart
|
|
{
|
|
formatter.removeUsed(from: &argNames, with: &nameIndexes, in: whereIndex + 1 ..< bodyStart)
|
|
}
|
|
|
|
// Replace unused bindings with `_`
|
|
for nameIndex in nameIndexes.reversed() {
|
|
formatter.replaceToken(at: nameIndex, with: .identifier("_"))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Closure arguments
|
|
formatter.forEach(.keyword("in")) { i, _ in
|
|
var argNames = [String]()
|
|
var nameIndexPairs = [(Int, Int)]()
|
|
guard let start = formatter.index(of: .startOfScope("{"), before: i) else {
|
|
return
|
|
}
|
|
var index = i - 1
|
|
var argCountStack = [0]
|
|
while index > start {
|
|
let token = formatter.tokens[index]
|
|
switch token {
|
|
case .endOfScope("}"):
|
|
return
|
|
case .endOfScope("]"):
|
|
// TODO: handle unused capture list arguments
|
|
index = formatter.index(of: .startOfScope("["), before: index) ?? index
|
|
case .endOfScope(")"):
|
|
argCountStack.append(argNames.count)
|
|
case .startOfScope("("):
|
|
argCountStack.removeLast()
|
|
case .delimiter(","):
|
|
argCountStack[argCountStack.count - 1] = argNames.count
|
|
case .identifier("async") where
|
|
formatter.last(.nonSpaceOrLinebreak, before: index)?.isIdentifier == true:
|
|
fallthrough
|
|
case .operator("->", .infix), .keyword("throws"):
|
|
// Everything after this was part of return value
|
|
let count = argCountStack.last ?? 0
|
|
argNames.removeSubrange(count ..< argNames.count)
|
|
nameIndexPairs.removeSubrange(count ..< nameIndexPairs.count)
|
|
case let .keyword(name) where !token.isAttribute && !token.isMacro && name != "inout":
|
|
return
|
|
case .identifier:
|
|
guard argCountStack.count < 3,
|
|
let prevToken = formatter.last(.nonSpaceOrCommentOrLinebreak, before: index), [
|
|
.delimiter(","), .startOfScope("("), .startOfScope("{"), .endOfScope("]"),
|
|
].contains(prevToken), let scopeStart = formatter.index(of: .startOfScope, before: index),
|
|
![.startOfScope("["), .startOfScope("<")].contains(formatter.tokens[scopeStart])
|
|
else {
|
|
break
|
|
}
|
|
let name = token.unescaped()
|
|
if let nextIndex = formatter.index(of: .nonSpaceOrCommentOrLinebreak, after: index),
|
|
let nextToken = formatter.token(at: nextIndex), case .identifier = nextToken,
|
|
formatter.next(.nonSpaceOrCommentOrLinebreak, after: nextIndex) == .delimiter(":")
|
|
{
|
|
let internalName = nextToken.unescaped()
|
|
if internalName != "_" {
|
|
argNames.append(internalName)
|
|
nameIndexPairs.append((index, nextIndex))
|
|
}
|
|
} else if name != "_" {
|
|
argNames.append(name)
|
|
nameIndexPairs.append((index, index))
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
index -= 1
|
|
}
|
|
guard !argNames.isEmpty, let bodyEndIndex = formatter.index(of: .endOfScope("}"), after: i) else {
|
|
return
|
|
}
|
|
formatter.removeUsed(from: &argNames, with: &nameIndexPairs, in: i + 1 ..< bodyEndIndex)
|
|
for pair in nameIndexPairs {
|
|
if case .identifier("_") = formatter.tokens[pair.0], pair.0 != pair.1 {
|
|
formatter.removeToken(at: pair.1)
|
|
if formatter.tokens[pair.1 - 1] == .space(" ") {
|
|
formatter.removeToken(at: pair.1 - 1)
|
|
}
|
|
} else {
|
|
formatter.replaceToken(at: pair.1, with: .identifier("_"))
|
|
}
|
|
}
|
|
}
|
|
} examples: {
|
|
"""
|
|
```diff
|
|
- func foo(bar: Int, baz: String) {
|
|
print("Hello \\(baz)")
|
|
}
|
|
|
|
+ func foo(bar _: Int, baz: String) {
|
|
print("Hello \\(baz)")
|
|
}
|
|
```
|
|
|
|
```diff
|
|
- func foo(_ bar: Int) {
|
|
...
|
|
}
|
|
|
|
+ func foo(_: Int) {
|
|
...
|
|
}
|
|
```
|
|
|
|
```diff
|
|
- request { response, data in
|
|
self.data += data
|
|
}
|
|
|
|
+ request { _, data in
|
|
self.data += data
|
|
}
|
|
```
|
|
|
|
```diff
|
|
- for (key, value) in dictionary {
|
|
print(key)
|
|
}
|
|
|
|
+ for (key, _) in dictionary {
|
|
print(key)
|
|
}
|
|
```
|
|
"""
|
|
}
|
|
}
|
|
|
|
extension Formatter {
|
|
func removeUsed(from argNames: inout [String], with associatedData: inout [some Any],
|
|
locals: Set<String> = [], in range: CountableRange<Int>)
|
|
{
|
|
var isDeclaration = false
|
|
var wasDeclaration = false
|
|
var isConditional = false
|
|
var isGuard = false
|
|
var locals = locals
|
|
var tempLocals = Set<String>()
|
|
func pushLocals() {
|
|
if isDeclaration, isConditional {
|
|
for name in tempLocals {
|
|
if let index = argNames.firstIndex(of: name),
|
|
!locals.contains(name)
|
|
{
|
|
argNames.remove(at: index)
|
|
associatedData.remove(at: index)
|
|
}
|
|
}
|
|
}
|
|
wasDeclaration = isDeclaration
|
|
isDeclaration = false
|
|
locals.formUnion(tempLocals)
|
|
tempLocals.removeAll()
|
|
}
|
|
var i = range.lowerBound
|
|
while i < range.upperBound {
|
|
if isStartOfStatement(at: i, treatingCollectionKeysAsStart: false),
|
|
// Immediately following an `=` operator, if or switch keywords
|
|
// are expressions rather than statements.
|
|
lastToken(before: i, where: { !$0.isSpaceOrCommentOrLinebreak })?.isOperator("=") != true
|
|
{
|
|
pushLocals()
|
|
wasDeclaration = false
|
|
}
|
|
let token = tokens[i]
|
|
outer: switch token {
|
|
case .keyword("guard"):
|
|
isGuard = true
|
|
case .keyword("let"), .keyword("var"), .keyword("func"), .keyword("for"):
|
|
isDeclaration = true
|
|
var i = i
|
|
while let scopeStart = index(of: .startOfScope("("), before: i) {
|
|
i = scopeStart
|
|
}
|
|
isConditional = isConditionalStatement(at: i)
|
|
case .identifier:
|
|
let name = token.unescaped()
|
|
guard let index = argNames.firstIndex(of: name), !locals.contains(name) else {
|
|
break
|
|
}
|
|
if last(.nonSpaceOrCommentOrLinebreak, before: i)?.isOperator(".") == false,
|
|
next(.nonSpaceOrCommentOrLinebreak, after: i) != .delimiter(":") || startOfScope(at: i).map({
|
|
scopeType(at: $0) == .dictionary
|
|
}) ?? false
|
|
{
|
|
if isDeclaration {
|
|
switch next(.nonSpaceOrCommentOrLinebreak, after: i) {
|
|
case .delimiter(",")? where !isConditional, .endOfScope(")")?, .operator("=", .infix)?:
|
|
tempLocals.insert(name)
|
|
break outer
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
argNames.remove(at: index)
|
|
associatedData.remove(at: index)
|
|
if argNames.isEmpty {
|
|
return
|
|
}
|
|
}
|
|
case .keyword("if"), .keyword("switch"):
|
|
guard isConditionalAssignment(at: i),
|
|
let conditinalBranches = conditionalBranches(at: i),
|
|
let endIndex = conditinalBranches.last?.endOfBranch
|
|
else { fallthrough }
|
|
|
|
removeUsed(from: &argNames, with: &associatedData,
|
|
locals: locals, in: i + 1 ..< endIndex)
|
|
case .startOfScope("{"):
|
|
guard let endIndex = endOfScope(at: i) else {
|
|
return fatalError("Expected }", at: i)
|
|
}
|
|
if isStartOfClosure(at: i) {
|
|
removeUsed(from: &argNames, with: &associatedData,
|
|
locals: locals, in: i + 1 ..< endIndex)
|
|
} else if isGuard {
|
|
removeUsed(from: &argNames, with: &associatedData,
|
|
locals: locals, in: i + 1 ..< endIndex)
|
|
pushLocals()
|
|
} else {
|
|
let prevLocals = locals
|
|
pushLocals()
|
|
removeUsed(from: &argNames, with: &associatedData,
|
|
locals: locals, in: i + 1 ..< endIndex)
|
|
locals = prevLocals
|
|
}
|
|
|
|
isGuard = false
|
|
i = endIndex
|
|
case .endOfScope("case"), .endOfScope("default"):
|
|
pushLocals()
|
|
guard let colonIndex = index(of: .startOfScope(":"), after: i) else {
|
|
return fatalError("Expected :", at: i)
|
|
}
|
|
guard let endIndex = endOfScope(at: colonIndex) else {
|
|
return fatalError("Expected end of case statement",
|
|
at: colonIndex)
|
|
}
|
|
removeUsed(from: &argNames, with: &associatedData,
|
|
locals: locals, in: i + 1 ..< endIndex)
|
|
i = endIndex - 1
|
|
case .operator("=", .infix), .delimiter(":"), .startOfScope(":"),
|
|
.keyword("in"), .keyword("where"):
|
|
wasDeclaration = isDeclaration
|
|
isDeclaration = false
|
|
case .delimiter(","):
|
|
if let scope = currentScope(at: i), [
|
|
.startOfScope("("), .startOfScope("["), .startOfScope("<"),
|
|
].contains(scope) {
|
|
break
|
|
}
|
|
if isConditional {
|
|
if isGuard, wasDeclaration {
|
|
pushLocals()
|
|
}
|
|
wasDeclaration = false
|
|
} else {
|
|
let _wasDeclaration = wasDeclaration
|
|
pushLocals()
|
|
isDeclaration = _wasDeclaration
|
|
}
|
|
case .delimiter(";"):
|
|
pushLocals()
|
|
wasDeclaration = false
|
|
default:
|
|
break
|
|
}
|
|
i += 1
|
|
}
|
|
}
|
|
}
|