// // FormattingHelpers.swift // SwiftFormat // // Created by Nick Lockwood on 16/08/2020. // Copyright © 2020 Nick Lockwood. All rights reserved. // import Foundation // MARK: shared helper methods extension Formatter { /// should brace be wrapped according to `wrapMultilineStatementBraces` rule? func shouldWrapMultilineStatementBrace(at index: Int) -> Bool { assert(tokens[index] == .startOfScope("{")) guard let endIndex = endOfScope(at: index), tokens[index + 1 ..< endIndex].contains(where: \.isLinebreak), let prevIndex = self.index(of: .nonSpaceOrCommentOrLinebreak, before: index), let prevToken = token(at: prevIndex), !prevToken.isStartOfScope, !prevToken.isDelimiter else { return false } let indent = currentIndentForLine(at: prevIndex) guard isStartOfClosure(at: index) else { return indent > currentIndentForLine(at: endIndex) } if prevToken == .endOfScope(")"), !tokens[startOfLine(at: prevIndex, excludingIndent: true)].is(.endOfScope), let startIndex = self.index(of: .startOfScope("("), before: prevIndex), currentIndentForLine(at: startIndex) < indent { return !onSameLine(startIndex, prevIndex) } return false } /// Should the specified token be followed by a space if next token is an opening paren, bracket, etc? func shouldInsertSpaceAfterToken(at index: Int) -> Bool? { switch token(at: index) { case let .keyword(keywordOrAttribute): switch keywordOrAttribute { case "@autoclosure": if options.swiftVersion < "3", let nextIndex = self.index(of: .nonSpaceOrLinebreak, after: index), next(.nonSpaceOrCommentOrLinebreak, after: nextIndex) == .identifier("escaping") { assert(tokens[nextIndex] == .startOfScope("(")) return false } return true case "@escaping", "@noescape", "@Sendable", "@MainActor": return true case _ where keywordOrAttribute.isAttribute: if next(.nonSpaceOrCommentOrLinebreak, after: index) == .startOfScope("[") { return true } if let i = self.index(of: .startOfScope("("), after: index) { return isParameterList(at: i) } return false case "private", "fileprivate", "internal", "init", "subscript", "throws": return false case "await": return options.swiftVersion >= "5.5" || options.swiftVersion == .undefined default: return !keywordOrAttribute.isMacroOrAttribute } case let .identifier(name): switch name { case "as", "is", "try": // not treated as keywords inside macro return token(at: index - 1)?.isOperator(".") != true case "unsafe": return options.swiftVersion >= "6.2" || options.swiftVersion == .undefined default: return name.isKeywordInTypeContext && token(at: index - 1)?.isOperator(".") != true && isTypePosition(at: index) } case .endOfScope("]"): return isInClosureArguments(at: index) case .endOfScope(")"): if let openParenIndex = self.index(of: .startOfScope("("), before: index), let prevIndex = self.index(of: .nonSpaceOrCommentOrLinebreak, before: openParenIndex), tokens[prevIndex] == .identifier("nonisolated") { return true } return isAttribute(at: index) case .number, .endOfScope("}"), .endOfScope(">"): return false default: return nil } } /// remove self if possible func removeSelf(at i: Int, exclude: Set, include: Set? = nil) -> Bool { guard case let .identifier(selfKeyword) = tokens[i], ["self", "Self"].contains(selfKeyword) else { assertionFailure() return false } let staticSelf = selfKeyword == "Self" let exclusionList = exclude .union(_FormatRules.globalSwiftFunctions) .union(staticSelf ? [] : options.selfRequired) guard let dotIndex = index(of: .nonSpaceOrLinebreak, after: i, if: { $0 == .operator(".", .infix) }), !exclude.contains(selfKeyword), let nextIndex = index(of: .nonSpaceOrLinebreak, after: dotIndex), let token = token(at: nextIndex), token.isIdentifier, case let name = token.unescaped(), (include.map { $0.contains(name) } ?? true), !isSymbol(at: nextIndex, in: exclusionList), !backticksRequired(at: nextIndex, ignoreLeadingDot: true) else { return false } var index = i loop: while let scopeStart = self.index(of: .startOfScope, before: index) { switch tokens[scopeStart] { case .startOfScope("["): break case .startOfScope("("): if let prevIndex = self.index(of: .nonSpaceOrCommentOrLinebreak, before: scopeStart), isSymbol(at: prevIndex, in: staticSelf ? [] : options.selfRequired.union([ "expect", // Special case to support autoclosure arguments in the Nimble framework "os_log", // Special case to support string interpolation inside os_log ])) || isAttribute(at: prevIndex) { return false } case let token: if token.isStringDelimiter { break } break loop } index = scopeStart } if !staticSelf, isAssignedToSelfRequiredType(at: index) { return false } removeTokens(in: i ..< nextIndex) return true } /// Whether the expression at the given index is on the RHS of an assignment /// whose LHS has a type annotation matching a `selfRequired` type. /// e.g. `let _: OSLogMessage = "\(self.bar)"` or `let _: OSLogMessage = foo(self.bar)` func isAssignedToSelfRequiredType(at i: Int) -> Bool { guard !options.selfRequired.isEmpty else { return false } // Walk backwards from start of expression to find `=` operator var index = i while let prevIndex = self.index(of: .nonSpaceOrCommentOrLinebreak, before: index) { if tokens[prevIndex] == .operator("=", .infix) { return isSelfRequiredType(beforeAssignment: prevIndex) } switch tokens[prevIndex] { case .identifier, .operator(".", .infix), .keyword("try"), .keyword("await"), .operator("?", .postfix), .operator("!", .postfix): index = prevIndex default: return false } } return false } /// Whether the type annotation before an `=` operator is a `selfRequired` type. func isSelfRequiredType(beforeAssignment equalsIndex: Int) -> Bool { var typeIndex = equalsIndex while let prevTypeIndex = index(of: .nonSpaceOrCommentOrLinebreak, before: typeIndex) { if tokens[prevTypeIndex].isUnwrapOperator { typeIndex = prevTypeIndex } else if tokens[prevTypeIndex] == .endOfScope(">"), let matchingStart = startOfScope(at: prevTypeIndex) { typeIndex = matchingStart } else { typeIndex = prevTypeIndex break } } guard tokens[typeIndex].isIdentifier else { return false } return options.selfRequired.contains(tokens[typeIndex].unescaped()) } /// gather declared variable names, starting at index after let/var keyword func processDeclaredVariables(at index: inout Int, names: inout Set, removeSelfKeyword: String?, onlyLocal: Bool, scopeAllowsImplicitSelfRebinding: Bool) { let isConditional = isConditionalStatement(at: index) var declarationIndex: Int? = -1 var scopeIndexStack = [Int]() var locals = Set() while let token = token(at: index) { outer: switch token { case let .identifier(name) where last(.nonSpace, before: index)?.isOperator == false: if name == removeSelfKeyword, isEnabled, let nextIndex = self.index( of: .nonSpaceOrCommentOrLinebreak, after: index, if: { $0 == .operator(".", .infix) } ), case .identifier? = next( .nonSpaceOrComment, after: nextIndex ) { _ = removeSelf(at: index, exclude: names.union(locals)) break } switch next(.nonSpaceOrCommentOrLinebreak, after: index) { case .delimiter(":")? where !scopeIndexStack.isEmpty, .operator(".", _)?: break outer default: break } let name = token.unescaped() // Whether or not this property is a `let self` definition // that rebinds implicit self for the remainder of scope. // This is only permitted in `weak self` closures when // unwrapping self like `let self = self`. var isPermittedImplicitSelfRebinding = false if name == "self", scopeAllowsImplicitSelfRebinding, let equalsIndex = self.index(of: .nonSpaceOrCommentOrLinebreak, after: index) { // If we find the end of the condition instead of an = token, // then this was a shorthand `if let self` condition. if tokens[equalsIndex] == .startOfScope("{") || tokens[equalsIndex] == .delimiter(",") || tokens[equalsIndex] == .keyword("else") { isPermittedImplicitSelfRebinding = true } else if tokens[equalsIndex] == Token.operator("=", .infix), let rhsSelfIndex = self.index(of: .nonSpaceOrCommentOrLinebreak, after: equalsIndex), tokens[rhsSelfIndex] == .identifier("self"), let nextToken = next(.nonSpaceOrCommentOrLinebreak, after: rhsSelfIndex), nextToken == .startOfScope("{") || nextToken == .delimiter(",") || nextToken == .keyword("else") { isPermittedImplicitSelfRebinding = true } } if name != "_", declarationIndex != nil || !isConditional { if isPermittedImplicitSelfRebinding { assert(name == "self") names.remove("self") } else { locals.insert(name) } } inner: while let nextIndex = self.index(of: .nonSpace, after: index) { let token = tokens[nextIndex] if isStartOfStatement(at: nextIndex) { names.formUnion(locals) } let removeSelfKeyword = isEnabled && ( options.swiftVersion >= "5.4" || isConditionalStatement(at: nextIndex) ) ? removeSelfKeyword : nil let include = onlyLocal ? locals : nil switch token { case .keyword("is"), .keyword("as"), .keyword("try"), .keyword("await"): break case .identifier(removeSelfKeyword ?? ""): _ = removeSelf(at: nextIndex, exclude: names, include: include) case .startOfScope("<"), .startOfScope("["), .startOfScope("("), .startOfScope where token.isStringDelimiter: guard let endIndex = endOfScope(at: nextIndex) else { return fatalError("Expected end of scope", at: nextIndex) } if let removeSelfKeyword { var i = endIndex - 1 while i > nextIndex { switch tokens[i] { case .endOfScope("}"): i = self.index(of: .startOfScope("{"), before: i) ?? i case .identifier(removeSelfKeyword): _ = removeSelf(at: i, exclude: names, include: include) default: break } i -= 1 } index = endOfScope(at: nextIndex)! } else { index = endIndex } fallthrough case .number, .identifier: index = max(index, nextIndex) if next(.nonSpaceOrCommentOrLinebreak, after: index, if: { $0.isOperator(ofType: .infix) || $0.isOperator(ofType: .postfix) || [ .keyword("is"), .keyword("as"), .delimiter(","), .startOfScope("["), .startOfScope("("), ].contains($0) }) == nil { names.formUnion(locals) return } continue inner case .keyword("switch"): // Handle switch expressions (SE-0380) guard let braceIndex = self.index(of: .startOfScope("{"), after: nextIndex), let endIndex = endOfScope(at: braceIndex) else { names.formUnion(locals) return } index = endIndex continue inner case .keyword("let"), .keyword("var"): names.formUnion(locals) declarationIndex = nextIndex index = nextIndex break inner case .keyword, .startOfScope("{"), .endOfScope("}"), .startOfScope(":"): names.formUnion(locals) return case .endOfScope(")"): let scopeIndex = scopeIndexStack.popLast() ?? -1 if let d = declarationIndex, d > scopeIndex { declarationIndex = nil } case .delimiter(","): if let d = declarationIndex, d >= scopeIndexStack.last ?? -1 { declarationIndex = nil } index = nextIndex names.formUnion(locals) break inner case .endOfScope("*/"), .linebreak: updateEnablement(at: nextIndex) default: break } index = nextIndex } case .keyword("let"), .keyword("var"): declarationIndex = index case .startOfScope("("): guard declarationIndex == nil else { scopeIndexStack.append(index) break } guard let endIndex = self.index(of: .endOfScope(")"), after: index) else { return fatalError("Expected )", at: index) } guard tokens[index ..< endIndex].contains(where: { [.keyword("let"), .keyword("var")].contains($0) }) else { index = endIndex break } scopeIndexStack.append(index) case .startOfScope("{"): guard isStartOfClosure(at: index), let nextIndex = endOfScope(at: index) else { index -= 1 names.formUnion(locals) return } index = nextIndex case .endOfScope("*/"), .linebreak: updateEnablement(at: index) default: break } index += 1 } } /// Shared wrap implementation func wrapCollectionsAndArguments(completePartialWrapping: Bool, wrapSingleArguments: Bool) { let maxWidth = options.maxWidth let listWrapThreshold = options.listWrapThreshold let effectiveWrapThreshold = listWrapThreshold ?? maxWidth func removeLinebreakBeforeEndOfScope(at endOfScope: inout Int) { guard let lastIndex = index(of: .nonSpace, before: endOfScope, if: { $0.isLinebreak }) else { return } if case .commentBody? = last(.nonSpace, before: lastIndex) { return } // Remove linebreak removeTokens(in: lastIndex ..< endOfScope) endOfScope = lastIndex // Remove trailing comma if let prevCommaIndex = index(of: .nonSpaceOrCommentOrLinebreak, before: endOfScope, if: { $0 == .delimiter(",") }) { removeToken(at: prevCommaIndex) endOfScope -= 1 } } func keepParameterLabelsOnSameLine(startOfScope i: Int, endOfScope: inout Int) { var endIndex = endOfScope while let index = self.lastIndex(of: .linebreak, in: i + 1 ..< endIndex) { endIndex = index // Check if this linebreak sits between two identifiers // (e.g. the external and internal argument labels) guard let lastIndex = self.index(of: .nonSpaceOrLinebreak, before: index, if: { $0.isIdentifier }), let nextIndex = self.index(of: .nonSpaceOrLinebreak, after: index, if: { $0.isIdentifier }) else { continue } // Remove linebreak let range = lastIndex + 1 ..< nextIndex let linebreakAndIndent = tokens[index ..< nextIndex] replaceTokens(in: range, with: .space(" ")) endOfScope -= (range.count - 1) // Insert replacement linebreak after next comma if let nextComma = self.index(of: .delimiter(","), after: index) { if token(at: nextComma + 1)?.isSpace == true { replaceToken(at: nextComma + 1, with: linebreakAndIndent) endOfScope += linebreakAndIndent.count - 1 } else { insert(Array(linebreakAndIndent), at: nextComma + 1) endOfScope += linebreakAndIndent.count } } } } func wrapReturnAndEffectsIfNecessary( startOfScope: Int, endOfFunctionScope: Int ) { guard token(at: startOfScope) == .startOfScope("(") else { return } let closingParenLine = startOfLine(at: endOfFunctionScope) var cursorIndex = endOfFunctionScope + 1 var shouldUnwrapReturnArrow = false if let parsedEffects = parseFunctionDeclarationEffectsClause(at: cursorIndex) { let effectsIndex = parsedEffects.range.lowerBound cursorIndex = parsedEffects.range.upperBound + 1 switch options.wrapEffects { case .preserve: break case .ifMultiline: // If the effect is on the same line as the closing paren, wrap it guard closingParenLine == startOfLine(at: effectsIndex) else { break } cursorIndex += wrapLine(before: effectsIndex) shouldUnwrapReturnArrow = true case .never: cursorIndex += unwrapLine(before: effectsIndex, preservingComments: false) } } if let parsedReturn = parseFunctionDeclarationReturnClause(at: cursorIndex) { let arrowIndex = parsedReturn.returnOperatorIndex cursorIndex = parsedReturn.returnType.range.upperBound + 1 if shouldUnwrapReturnArrow { // this is part of the effects wrapping rule cursorIndex += unwrapLine(before: arrowIndex, preservingComments: false) } switch options.wrapReturnType { case .preserve: break case .ifMultiline: // If the return arrow is on the same line as the closing paren, wrap it guard closingParenLine == startOfLine(at: arrowIndex) else { break } cursorIndex += wrapLine(before: arrowIndex) case .never: cursorIndex += unwrapLine(before: arrowIndex, preservingComments: true) // TODO: handle where clause if let nextIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: cursorIndex - 1), tokens[nextIndex] == .startOfScope("{") { // Don't unwrap the brace line if using Allman braces if !options.allmanBraces { unwrapLine(before: nextIndex, preservingComments: true) } } } } } func wrapArgumentsBeforeFirst(startOfScope i: Int, endOfScope: Int, allowGrouping: Bool) { // Get indent let indent = currentIndentForLine(at: i) var endOfScope = endOfScope keepParameterLabelsOnSameLine(startOfScope: i, endOfScope: &endOfScope) let closingParenOnSameLine: Bool if isFunctionCall(at: i) { switch options.callSiteClosingParenPosition { case .balanced: closingParenOnSameLine = false case .sameLine: closingParenOnSameLine = true case .default: closingParenOnSameLine = options.closingParenPosition == .sameLine } } else if tokens[i] == .startOfScope("(") { switch options.closingParenPosition { case .balanced: closingParenOnSameLine = false case .sameLine: closingParenOnSameLine = true case .default: closingParenOnSameLine = false } } else { closingParenOnSameLine = false } // Insert linebreak after each comma var index = index(of: .nonSpaceOrCommentOrLinebreak, before: endOfScope)! if tokens[index] != .delimiter(",") { index += 1 } while let commaIndex = self.lastIndex(of: .delimiter(","), in: i + 1 ..< index), var linebreakIndex = self.index(of: .nonSpaceOrComment, after: commaIndex) { if let index = self.index(of: .nonSpace, before: linebreakIndex) { linebreakIndex = index + 1 } if !isCommentedCode(at: linebreakIndex + 1) { if tokens[linebreakIndex].isLinebreak, next(.nonSpace, after: linebreakIndex).map({ !$0.isLinebreak }) ?? false { endOfScope += insertSpace(indent + options.indent, at: linebreakIndex + 1) } else if !allowGrouping || (maxWidth > 0 && lineLength(at: linebreakIndex) > maxWidth && lineLength(upTo: linebreakIndex) <= maxWidth), !tokens[linebreakIndex].isLinebreak { insertLinebreak(at: linebreakIndex) endOfScope += 1 + insertSpace(indent + options.indent, at: linebreakIndex + 1) } } index = commaIndex } // If the closing paren is on the same line, and there's only a single item in the list, // don't insert an opening paren (unless we're over the line width limit). This prevents // issues with an open paren being wrapped unnecessarily and sitting on its own line in // cases like long closure types in parens. let insertLinebreakAfterOpeningParen = self.index(of: .delimiter(","), after: i) != nil || (maxWidth > 0 && lineLength(at: endOfLine(at: i)) > maxWidth) || listWrapThreshold.map { lineLength(at: endOfLine(at: i)) > $0 } ?? false // Insert linebreak and indent after opening paren if insertLinebreakAfterOpeningParen, let nextIndex = self.index(of: .nonSpaceOrComment, after: i) { if !tokens[nextIndex].isLinebreak { insertLinebreak(at: nextIndex) endOfScope += 1 } if nextIndex + 1 < endOfScope, next(.nonSpace, after: nextIndex)?.isLinebreak == false { var indent = indent if let nextNonSpaceIndex = self.index(of: .nonSpace, after: nextIndex), nextNonSpaceIndex < endOfScope, !isCommentedCode(at: nextNonSpaceIndex) { indent += options.indent } endOfScope += insertSpace(indent, at: nextIndex + 1) } } let hasLineBreakAfterOpeningParen = nextToken(after: i, where: { !$0.isComment })?.isLinebreak == true if closingParenOnSameLine { removeLinebreakBeforeEndOfScope(at: &endOfScope) } else if hasLineBreakAfterOpeningParen { // Insert linebreak before closing paren if let lastIndex = self.index(of: .nonSpace, before: endOfScope) { endOfScope += insertSpace(indent, at: lastIndex + 1) if !tokens[lastIndex].isLinebreak { insertLinebreak(at: lastIndex + 1) endOfScope += 1 } } } wrapReturnAndEffectsIfNecessary( startOfScope: i, endOfFunctionScope: endOfScope ) } func wrapArgumentsAfterFirst(startOfScope i: Int, endOfScope: Int, allowGrouping: Bool) { guard var firstArgumentIndex = index(of: .nonSpaceOrLinebreak, in: i + 1 ..< endOfScope) else { return } var endOfScope = endOfScope keepParameterLabelsOnSameLine(startOfScope: i, endOfScope: &endOfScope) // Remove linebreak after opening paren removeTokens(in: i + 1 ..< firstArgumentIndex) endOfScope -= (firstArgumentIndex - (i + 1)) firstArgumentIndex = i + 1 // Get indent let start = startOfLine(at: i) let indent = spaceEquivalentToTokens(from: start, upTo: firstArgumentIndex) removeLinebreakBeforeEndOfScope(at: &endOfScope) // Insert linebreak after each comma var lastBreakIndex: Int? var index = firstArgumentIndex while let commaIndex = self.index(of: .delimiter(","), in: index ..< endOfScope), var linebreakIndex = self.index(of: .nonSpaceOrComment, after: commaIndex) { if let index = self.index(of: .nonSpace, before: linebreakIndex) { linebreakIndex = index + 1 } if maxWidth > 0, lineLength(upTo: commaIndex) >= maxWidth, let breakIndex = lastBreakIndex { endOfScope += 1 + insertSpace(indent, at: breakIndex) insertLinebreak(at: breakIndex) lastBreakIndex = nil index = commaIndex + 1 continue } if tokens[linebreakIndex].isLinebreak { if linebreakIndex + 1 != endOfScope, !isCommentedCode(at: linebreakIndex + 1) { endOfScope += insertSpace(indent, at: linebreakIndex + 1) } } else if !allowGrouping { insertLinebreak(at: linebreakIndex) endOfScope += 1 + insertSpace(indent, at: linebreakIndex + 1) } else { lastBreakIndex = linebreakIndex } index = commaIndex + 1 } if maxWidth > 0, let breakIndex = lastBreakIndex, lineLength(at: breakIndex) > maxWidth { insertSpace(indent, at: breakIndex) insertLinebreak(at: breakIndex) } wrapReturnAndEffectsIfNecessary( startOfScope: i, endOfFunctionScope: endOfScope ) } // Wrap nested structures like typealiases and ternary operators first since this may // prevent the need to do wrap the child expressions. // -- wraptypealiases forEach(.keyword("typealias")) { typealiasIndex, _ in guard options.wrapTypealiases == .beforeFirst || options.wrapTypealiases == .afterFirst, let (equalsIndex, andTokenIndices, lastIdentifierIndex) = parseProtocolCompositionTypealias(at: typealiasIndex) else { return } // Decide which indices to wrap at // - We always wrap at each `&` // - For `beforeFirst`, we also wrap before the `=` let wrapIndices: [Int] switch options.wrapTypealiases { case .afterFirst: wrapIndices = andTokenIndices case .beforeFirst: wrapIndices = [equalsIndex] + andTokenIndices case .default, .disabled, .preserve: return } let didWrap = wrapMultilineStatement( startIndex: typealiasIndex, delimiterIndices: wrapIndices, endIndex: lastIdentifierIndex ) guard didWrap else { return } // If we're using `afterFirst` and there was unexpectedly a linebreak // between the `typealias` and the `=`, we need to remove it let rangeBetweenTypealiasAndEquals = (typealiasIndex + 1) ..< equalsIndex if options.wrapTypealiases == .afterFirst, let linebreakIndex = rangeBetweenTypealiasAndEquals.first(where: { tokens[$0].isLinebreak }) { removeToken(at: linebreakIndex) if tokens[linebreakIndex].isSpace, tokens[linebreakIndex] != .space(" ") { replaceToken(at: linebreakIndex, with: .space(" ")) } } } // --wrapternary forEach(.operator("?", .infix)) { conditionIndex, _ in guard options.wrapTernaryOperators != .default, let expressionStartIndex = index(of: .nonSpaceOrCommentOrLinebreak, before: conditionIndex), !isInStringLiteralWithWrappingDisabled(at: conditionIndex) else { return } // Find the : operator that separates the true and false branches // of this ternary operator // - You can have nested ternary operators, so the immediate-next colon // is not necessarily the colon of _this_ ternary operator. // - To track nested ternary operators, we maintain a count of // the unterminated `?` tokens that we've seen. // - This ternary's colon token is the first colon we find // where there isn't an unterminated `?`. var unterimatedTernaryCount = 0 var currentIndex = conditionIndex + 1 var foundColonIndex: Int? while foundColonIndex == nil, currentIndex < tokens.count { switch tokens[currentIndex] { case .operator("?", .infix): unterimatedTernaryCount += 1 case .operator(":", .infix): if unterimatedTernaryCount == 0 { foundColonIndex = currentIndex } else { unterimatedTernaryCount -= 1 } default: break } currentIndex += 1 } guard let colonIndex = foundColonIndex, let endOfElseExpression = endOfExpression(at: colonIndex, upTo: []) else { return } wrapMultilineStatement( startIndex: expressionStartIndex, delimiterIndices: [conditionIndex, colonIndex], endIndex: endOfElseExpression ) } var lastIndex = -1 forEachToken(onlyWhereEnabled: false) { i, token in guard case let .startOfScope(string) = token else { return } guard ["(", "[", "<"].contains(string) else { lastIndex = i return } if lastIndex < i, let i = (lastIndex + 1 ..< i).last(where: { tokens[$0].isLinebreak }) { lastIndex = i } guard let endOfScope = endOfScope(at: i) else { return } let mode: WrapMode let hasMultipleArguments = index(of: .delimiter(","), in: i + 1 ..< endOfScope) != nil let hasSomeArguments = index(of: .nonSpaceOrCommentOrLinebreak, in: i + 1 ..< endOfScope) != nil var isParameters = false switch string { case "(": // Don't wrap color/image literals due to Xcode bug guard let prevToken = self.token(at: i - 1), prevToken != .keyword("#colorLiteral"), prevToken != .keyword("#imageLiteral") else { return } guard hasMultipleArguments || wrapSingleArguments || index(in: i + 1 ..< endOfScope, where: { $0.isComment }) != nil else { // Not an argument list, or only one argument lastIndex = i return } // Don't wrap empty parameter lists for trivial functions // like `func foo() {`, but allow wrapping if the function has // a return type, effects, generics, etc. if index(of: .nonSpaceOrCommentOrLinebreak, in: i + 1 ..< endOfScope) == nil, isParameterList(at: i), let nextTokenIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: endOfScope), tokens[nextTokenIndex] == .startOfScope("{") { if isEnabled { // Unwrap linebreak before closing paren var mutableEndOfScope = endOfScope removeLinebreakBeforeEndOfScope(at: &mutableEndOfScope) } lastIndex = i return } isParameters = isParameterList(at: i) if isParameters, options.wrapParameters != .default { mode = options.wrapParameters } else { mode = options.wrapArguments } case "<": mode = options.wrapArguments case "[": mode = options.wrapCollections default: return } guard mode != .disabled, let firstIdentifierIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: i), !isInStringLiteralWithWrappingDisabled(at: i) else { lastIndex = i return } guard isEnabled else { lastIndex = i return } if completePartialWrapping, let firstLinebreakIndex = index(of: .linebreak, in: i + 1 ..< endOfScope) { switch mode { case .beforeFirst: wrapArgumentsBeforeFirst(startOfScope: i, endOfScope: endOfScope, allowGrouping: options.allowPartialWrapping && firstIdentifierIndex > firstLinebreakIndex) case .preserve where firstIdentifierIndex > firstLinebreakIndex: wrapArgumentsBeforeFirst(startOfScope: i, endOfScope: endOfScope, allowGrouping: options.allowPartialWrapping) case .afterFirst, .preserve: wrapArgumentsAfterFirst(startOfScope: i, endOfScope: endOfScope, allowGrouping: options.allowPartialWrapping) case .disabled, .default: assertionFailure() // Shouldn't happen } } else if maxWidth > 0 || listWrapThreshold != nil, hasMultipleArguments || wrapSingleArguments { func willWrapAtStartOfReturnType(maxWidth: Int) -> Bool { isInReturnType(at: i) && maxWidth < lineLength(at: i) } func startOfNextScopeNotInReturnType() -> Int? { let endOfLine = self.endOfLine(at: i) guard endOfScope < endOfLine else { return nil } var startOfLastScopeOnLine = endOfScope repeat { guard let startOfNextScope = index( of: .startOfScope, in: startOfLastScopeOnLine + 1 ..< endOfLine ) else { return nil } startOfLastScopeOnLine = startOfNextScope } while isInReturnType(at: startOfLastScopeOnLine) return startOfLastScopeOnLine } func indexOfNextWrap() -> Int? { let startOfNextScopeOnLine = startOfNextScopeNotInReturnType() let nextNaturalWrap = indexWhereLineShouldWrap(from: endOfScope + 1) switch (startOfNextScopeOnLine, nextNaturalWrap) { case let (.some(startOfNextScopeOnLine), .some(nextNaturalWrap)): return min(startOfNextScopeOnLine, nextNaturalWrap) case let (nil, .some(nextNaturalWrap)): return nextNaturalWrap case let (.some(startOfNextScopeOnLine), nil): return startOfNextScopeOnLine case (nil, nil): return nil } } func wrapArgumentsWithoutPartialWrapping() { switch mode { case .preserve, .beforeFirst: wrapArgumentsBeforeFirst(startOfScope: i, endOfScope: endOfScope, allowGrouping: false) case .afterFirst: wrapArgumentsAfterFirst(startOfScope: i, endOfScope: endOfScope, allowGrouping: true) case .disabled, .default: assertionFailure() // Shouldn't happen } } if currentRule == .wrap { let nextWrapIndex = indexOfNextWrap() ?? endOfLine(at: i) let lineLen = lineLength(upTo: nextWrapIndex) let exceedsMaxWidth = maxWidth > 0 && maxWidth < lineLen if nextWrapIndex > lastIndex, effectiveWrapThreshold < lineLen, !willWrapAtStartOfReturnType(maxWidth: effectiveWrapThreshold), hasSomeArguments || (wrapSingleArguments && exceedsMaxWidth) { wrapArgumentsWithoutPartialWrapping() lastIndex = nextWrapIndex return } } else { let lineLen = lineLength(upTo: endOfScope) let exceedsMaxWidth = maxWidth > 0 && maxWidth < lineLen if effectiveWrapThreshold < lineLen, hasSomeArguments || (wrapSingleArguments && exceedsMaxWidth) { wrapArgumentsWithoutPartialWrapping() } } } lastIndex = i } // -- wrapconditions forEach(.keyword) { index, token in let indent: String let endOfConditionsToken: Token switch token { case .keyword("guard"): endOfConditionsToken = .keyword("else") indent = " " case .keyword("if"): endOfConditionsToken = .startOfScope("{") indent = " " case .keyword("while"): endOfConditionsToken = .startOfScope("{") indent = " " default: return } // Only wrap when this is a control flow condition that spans multiple lines guard let endIndex = self.index(of: endOfConditionsToken, after: index), let nextTokenIndex = self.index(of: .nonSpaceOrLinebreak, after: index), !(onSameLine(index, endIndex) || self.index(of: .nonSpaceOrLinebreak, after: endOfLine(at: index)) == endIndex) else { return } switch options.wrapConditions { case .preserve, .disabled, .default: break case .beforeFirst: // Wrap if the next non-whitespace-or-comment // is on the same line as the control flow keyword if onSameLine(index, nextTokenIndex) { insertLinebreak(at: index + 1) } // Re-indent lines let keywordIndent = currentIndentForLine(at: index) var linebreakIndex: Int? = index + 1 let indent = keywordIndent + options.indent while let index = linebreakIndex, index < endIndex { if self.index(of: .nonSpaceOrLinebreak, after: index) == endIndex { insertSpace(keywordIndent, at: index + 1) } else { insertSpace(indent, at: index + 1) } linebreakIndex = self.index(of: .linebreak, after: index) } case .afterFirst: // Unwrap if the next non-whitespace-or-comment // is not on the same line as the control flow keyword if !onSameLine(index, nextTokenIndex), let linebreakIndex = self.index(of: .linebreak, in: index ..< nextTokenIndex) { removeToken(at: linebreakIndex) } // Make sure there is exactly one space after control flow keyword insertSpace(" ", at: index + 1) // Re-indent lines let keywordIndent = currentIndentForLine(at: index) var lastIndex = index + 1 let indent = spaceEquivalentToTokens(from: startOfLine(at: index), upTo: index) + indent while let index = self.index(of: .linebreak, after: lastIndex), index < endIndex { if self.index(of: .nonSpaceOrLinebreak, after: index) == endIndex { insertSpace(keywordIndent, at: index + 1) } else { insertSpace(indent, at: index + 1) } lastIndex = index } } } /// Wraps / re-wraps a multi-line statement where each delimiter index /// should be the first token on its line, if the statement /// is longer than the max width or there is already a linebreak /// adjacent to one of the delimiters @discardableResult func wrapMultilineStatement( startIndex: Int, delimiterIndices: [Int], endIndex: Int ) -> Bool { // ** Decide whether or not this statement needs to be wrapped / re-wrapped let range = startOfLine(at: startIndex) ... endIndex let length = tokens[range].map(\.string).joined().count // Only wrap if this line if longer than the max width... let overMaximumWidth = maxWidth > 0 && length > maxWidth // ... or if there is at least one delimiter currently adjacent to a linebreak, // which means this statement is already being wrapped in some way // and should be re-wrapped to the expected way if necessary let delimitersAdjacentToLinebreak = delimiterIndices.filter { delimiterIndex in last(.nonSpaceOrComment, before: delimiterIndex)?.is(.linebreak) == true || next(.nonSpaceOrComment, after: delimiterIndex)?.is(.linebreak) == true }.count if !(overMaximumWidth || delimitersAdjacentToLinebreak > 0) { return false } // ** Now that we know this is supposed to wrap, // make sure each delimiter is the start of a line let indent = currentIndentForLine(at: startIndex) + options.indent for indexToWrap in delimiterIndices.reversed() { // if this item isn't already on its own line, then wrap it if last(.nonSpaceOrComment, before: indexToWrap)?.is(.linebreak) == false { // Remove the space immediately before this token if present, // so it isn't orphaned on the previous line once we wrap if tokens[indexToWrap - 1].isSpace { removeToken(at: indexToWrap - 1) } insertSpace(indent, at: indexToWrap - 1) insertLinebreak(at: indexToWrap - 1) // While we're here, make sure there's exactly one space after the delimiter let updatedAndIndex = indexToWrap + 1 if let nextExpressionIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: updatedAndIndex) { replaceTokens( in: (updatedAndIndex + 1) ..< nextExpressionIndex, with: .space(" ") ) } } } return true } } /// Returns the index where the `wrap` rule should add the next linebreak in the line at the selected index. /// /// If the line does not need to be wrapped, this will return `nil`. /// /// - Note: This checks the entire line from the start of the line, the linebreak may be an index preceding the /// `index` passed to the function. func indexWhereLineShouldWrapInLine(at index: Int) -> Int? { indexWhereLineShouldWrap(from: startOfLine(at: index, excludingIndent: true)) } func indexWhereLineShouldWrap(from index: Int) -> Int? { var lineLength = lineLength(upTo: index) var stringLiteralDepth = 0 var currentPriority = 0 var lastBreakPoint: Int? var lastBreakPointPriority = Int.min let maxWidth = options.maxWidth guard maxWidth > 0 else { return nil } func addBreakPoint(at i: Int, relativePriority: Int) { guard stringLiteralDepth == 0, currentPriority + relativePriority >= lastBreakPointPriority, !isInClosureArguments(at: i + 1), next(.nonSpace, after: i + 1) != .startOfScope("//") else { return } let i = self.index(of: .nonSpace, before: i + 1) ?? i if token(at: i + 1)?.isLinebreak == true || token(at: i)?.isLinebreak == true { return } lastBreakPoint = i lastBreakPointPriority = currentPriority + relativePriority } var i = index let endIndex = endOfLine(at: index) while i < endIndex { var token = tokens[i] switch token { case .linebreak: return nil case .keyword("#colorLiteral"), .keyword("#imageLiteral"): guard let startIndex = self.index(of: .startOfScope("("), after: i), let endIndex = endOfScope(at: startIndex) else { return nil // error } token = .space(spaceEquivalentToTokens(from: i, upTo: endIndex + 1)) // hack to get correct length i = endIndex case let .delimiter(string) where options.noWrapOperators.contains(string), let .operator(string, .infix) where options.noWrapOperators.contains(string): // TODO: handle as/is break case .delimiter(","): addBreakPoint(at: i, relativePriority: 0) case .operator("=", .infix) where self.token(at: i + 1)?.isSpace == true: addBreakPoint(at: i, relativePriority: -9) case .operator(".", .infix), .operator("::", .infix): addBreakPoint(at: i - 1, relativePriority: -2) case .operator("->", .infix): if isInReturnType(at: i) { currentPriority -= 5 } addBreakPoint(at: i - 1, relativePriority: -5) case .operator(_, .infix) where self.token(at: i + 1)?.isSpace == true: addBreakPoint(at: i, relativePriority: -3) case .startOfScope("{"): if !isStartOfClosure(at: i) || next(.keyword, after: i) != .keyword("in"), next(.nonSpace, after: i) != .endOfScope("}") { addBreakPoint(at: i, relativePriority: -6) } if isInReturnType(at: i) { currentPriority += 5 } currentPriority -= 6 case .endOfScope("}"): currentPriority += 6 if last(.nonSpace, before: i) != .startOfScope("{") { addBreakPoint(at: i - 1, relativePriority: -6) } case .startOfScope("("): currentPriority -= 7 case .endOfScope(")"): currentPriority += 7 case .startOfScope("["): currentPriority -= 8 case .endOfScope("]"): currentPriority += 8 case .startOfScope("<"): currentPriority -= 9 case .endOfScope(">"): currentPriority += 9 case .startOfScope where token.isStringDelimiter: stringLiteralDepth += 1 case .endOfScope where token.isStringDelimiter: stringLiteralDepth -= 1 case .keyword("else"), .keyword("where"): addBreakPoint(at: i - 1, relativePriority: -1) case .keyword("in"): if last(.keyword, before: i) == .keyword("for") { addBreakPoint(at: i, relativePriority: -11) break } addBreakPoint(at: i, relativePriority: -5 - currentPriority) default: break } lineLength += tokenLength(token) if lineLength > maxWidth, let breakPoint = lastBreakPoint, breakPoint < i, !isInStringLiteralWithWrappingDisabled(at: i) { return breakPoint } i += 1 } return nil } /// Wrap a single-line statement body onto multiple lines func wrapStatementBody(at i: Int) { assert(token(at: i) == .startOfScope("{")) guard !isInStringLiteralWithWrappingDisabled(at: i) else { return } var openBraceIndex = i // We need to make sure to move past any closures in the conditional while isStartOfClosure(at: openBraceIndex) { guard let endOfClosureIndex = index(of: .endOfScope("}"), after: openBraceIndex), let nextOpenBrace = index(of: .startOfScope("{"), after: endOfClosureIndex + 1) else { return } openBraceIndex = nextOpenBrace } guard var indexOfFirstTokenInNewScope = index(of: .nonSpaceOrComment, after: openBraceIndex), // If the scope is empty we don't need to do anything !tokens[indexOfFirstTokenInNewScope].isEndOfScope, // If there is already a newline after the brace we can just stop !tokens[indexOfFirstTokenInNewScope].isLinebreak else { return } insertLinebreak(at: indexOfFirstTokenInNewScope) if tokens[indexOfFirstTokenInNewScope - 1].isSpace { // We left behind a trailing space on the previous line so we should clean it up removeToken(at: indexOfFirstTokenInNewScope - 1) indexOfFirstTokenInNewScope -= 1 } let movedTokenIndex = indexOfFirstTokenInNewScope + 1 // We want the token to be indented one level more than the conditional is let indent = currentIndentForLine(at: i) + options.indent insertSpace(indent, at: movedTokenIndex) guard var closingBraceIndex = index(of: .endOfScope("}"), after: movedTokenIndex), !(movedTokenIndex ..< closingBraceIndex).contains(where: { tokens[$0].isLinebreak }) else { // The closing brace is already on its own line so we don't need to do anything else return } insertLinebreak(at: closingBraceIndex) let lineBreakIndex = closingBraceIndex closingBraceIndex += 1 let previousIndex = lineBreakIndex - 1 if tokens[previousIndex].isSpace { // We left behind a trailing space on the previous line so we should clean it up removeToken(at: previousIndex) closingBraceIndex -= 1 } // We want the closing brace at the same indentation level as conditional insertSpace(currentIndentForLine(at: i), at: closingBraceIndex) } /// Returns true if the token at the specified index is inside a single-line string literal (including inside an interpolation), /// which should never be wrapped, or in any string literal when string interpolation wrapping is disabled. func isInStringLiteralWithWrappingDisabled(at i: Int) -> Bool { var i = i while let startOfScope = startOfScope(at: i) { i = startOfScope if tokens[startOfScope].isStringDelimiter { if options.wrapStringInterpolation == .preserve { return true } else if !tokens[startOfScope].isMultilineStringDelimiter { // Single line strings can never have line break return true } } } return false } func removeParen(at index: Int) { func tokenOutsideParenRequiresSpacing(at index: Int) -> Bool { guard let token = token(at: index) else { return false } switch token { case .identifier, .keyword, .number, .startOfScope("#if"): return true default: return false } } func tokenInsideParenRequiresSpacing(at index: Int) -> Bool { guard let token = token(at: index) else { return false } switch token { case .operator, .startOfScope("{"), .endOfScope("}"): return true default: return tokenOutsideParenRequiresSpacing(at: index) } } let isStartOfScope = tokens[index].isStartOfScope let spaceBefore = token(at: index - 1)?.isSpace == true let spaceAfter = token(at: index + 1)?.isSpaceOrLinebreak == true removeToken(at: index) if isStartOfScope { if tokenOutsideParenRequiresSpacing(at: index - 1), tokenInsideParenRequiresSpacing(at: index) { if !spaceBefore, !spaceAfter { // Need to insert one insert(.space(" "), at: index) } } else if spaceAfter, spaceBefore { removeToken(at: index - 1) } } else { if tokenInsideParenRequiresSpacing(at: index - 1), tokenOutsideParenRequiresSpacing(at: index) { if !spaceBefore, !spaceAfter { // Need to insert one insert(.space(" "), at: index) } } else if spaceBefore { removeToken(at: index - 1) } } } /// Common implementation for the `hoistTry` and `hoistAwait` rules /// Hoists the first keyword of the specified type out of the specified scope func hoistEffectKeyword( _ keyword: String, inScopeAt scopeStart: Int, isEffectCapturingAt: (Int) -> Bool ) { assert([.startOfScope("("), .startOfScope("[")].contains(tokens[scopeStart])) assert(["try", "await"].contains(keyword)) func insertEffectKeyword(at index: Int) { var index = index while tokens[index].isSpaceOrLinebreak { index += 1 } if tokens[index] == .keyword(keyword) { return } insert([.keyword(keyword)], at: index) if let nextToken = token(at: index + 1), !nextToken.isSpace { insertSpace(" ", at: index + 1) } else { insertSpace(" ", at: index) } } func removeKeyword(at index: Int) { removeToken(at: index) if token(at: index)?.isSpace == true { removeToken(at: index) } } var indicesToRemove = [Int]() var prev = scopeStart while let i = index(of: .keyword(keyword), after: prev) { if token(at: i + 1)?.isUnwrapOperator == false { indicesToRemove.append(i) } prev = i } if indicesToRemove.isEmpty { return } var insertIndex = scopeStart loop: while let i = index(of: .nonSpace, before: insertIndex) { let prevToken = tokens[insertIndex] switch tokens[i] { case .identifier where [.startOfScope("("), .startOfScope("[")].contains(prevToken): if isEffectCapturingAt(i) { return } case let .operator(name, .infix) where name != "=": if [.startOfScope("("), .startOfScope("[")].contains(prevToken), isEffectCapturingAt(i) { return } case let .keyword(name) where name.isMacro && prevToken == .startOfScope("("): return case .keyword where tokens[i].isAttribute && prevToken == .startOfScope("("): return case .keyword("try") where keyword == "await": break loop case let .keyword(name) where ["is", "as", "try", "await"].contains(name): break case .operator(_, .prefix), .stringBody, .endOfScope(")") where prevToken.isStringBody || (prevToken.isEndOfScope && prevToken.isStringDelimiter), .startOfScope where tokens[i].isStringDelimiter: break case _ where tokens[i].isUnwrapOperator: if last(.nonSpaceOrComment, before: i) == .keyword("try") { if keyword == "try" { // Can't merge try? and try return } break loop } if prevToken != .startOfScope("("), prevToken != .startOfScope("[") { fallthrough } case .operator(_, .postfix), .identifier, .number, .endOfScope(">"), .endOfScope("]"), .endOfScope(")"), .endOfScope where tokens[i].isStringDelimiter: switch prevToken { case .operator(_, .infix), .operator(_, .postfix), .stringBody, .linebreak, .startOfScope("<"), .startOfScope("["), .startOfScope("("), _ where currentScope(at: i + 1)?.isMultilineStringDelimiter == true: break default: break loop } if tokens[i].isEndOfScope { insertIndex = startOfScope(at: i) ?? i continue } case .linebreak: if prevToken.isOperator(ofType: .infix) || prevToken.isStringBody { break } else if let i = index(of: .nonSpaceOrLinebreak, before: i, if: { $0.isOperator(ofType: .infix) }) { insertIndex = i continue } else { break loop } default: break loop } insertIndex = i } indicesToRemove.reversed().forEach(removeKeyword(at:)) insertEffectKeyword(at: insertIndex) } /// Adds `throws` effect to the given function declaration if not already present func addThrowsEffect(to functionDecl: FunctionDeclaration) { guard !functionDecl.effects.contains("throws") else { return } if let effectsRange = functionDecl.effectsRange { // If async is present, insert throws after it to maintain correct order: async throws if let asyncIndex = index(of: .identifier("async"), in: effectsRange.lowerBound ..< effectsRange.upperBound + 1) { insert([.space(" "), .keyword("throws")], at: asyncIndex + 1) } else { // Otherwise add it to the end of effects insert([.keyword("throws"), .space(" ")], at: effectsRange.upperBound) } } else { // If there are no effects, add after the arguments insert([.space(" "), .keyword("throws")], at: functionDecl.argumentsRange.upperBound + 1) } } /// Removes the given `throws` or `async` effect from the given function declaration if present func removeEffect(_ effect: String, from functionDecl: FunctionDeclaration) { guard let effectsRange = functionDecl.effectsRange, let effectIndex = index(of: .keyword(effect), in: effectsRange) ?? index(of: .identifier(effect), in: effectsRange) else { return } var endIndex = effectIndex // Check if there's typed throws (throws(...)) if effect == "throws", let nextTokenIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: effectIndex), tokens[nextTokenIndex] == .startOfScope("("), let endOfScope = endOfScope(at: nextTokenIndex) { endIndex = endOfScope } // Include trailing whitespace if present if endIndex + 1 < tokens.count, tokens[endIndex + 1].isSpace { endIndex += 1 } removeTokens(in: effectIndex ... endIndex) } /// Whether or not the code block starting at the given `.startOfScope` token /// has a single statement. This makes it eligible to be used with implicit return. func blockBodyHasSingleStatement( atStartOfScope startOfScopeIndex: Int, includingConditionalStatements: Bool, includingReturnStatements: Bool, includingReturnInConditionalStatements: Bool? = nil ) -> Bool { guard let endOfScopeIndex = endOfScope(at: startOfScopeIndex) else { return false } let startOfBody = startOfBody(atStartOfScope: startOfScopeIndex) // The body should contain exactly one expression. // We can confirm this by parsing the body with `parseExpressionRange`, // and checking that the token after that expression is just the end of the scope. guard var firstTokenInBody = index(of: .nonSpaceOrCommentOrLinebreak, after: startOfBody) else { return false } // Skip over any optional `return` keyword if includingReturnStatements, tokens[firstTokenInBody] == .keyword("return") { guard let tokenAfterReturnKeyword = index(of: .nonSpaceOrCommentOrLinebreak, after: firstTokenInBody) else { return false } firstTokenInBody = tokenAfterReturnKeyword } // In Swift 5.9+, if and switch statements where each branch is a single statement // are also considered single statements if options.swiftVersion >= "5.9", includingConditionalStatements, let conditionalBranches = conditionalBranches(at: firstTokenInBody) { let isSupportedSingleStatement = conditionalBranches.allSatisfy { branch in // In Swift 5.9, there's a bug that prevents you from writing an // if or switch expression using an `as?` on one of the branches: // https://github.com/apple/swift/issues/68764 // // if condition { // foo as? String // } else { // "bar" // } // if conditionalBranchHasUnsupportedCastOperator(startOfScopeIndex: branch.startOfBranch) { return false } return blockBodyHasSingleStatement( atStartOfScope: branch.startOfBranch, includingConditionalStatements: true, includingReturnStatements: includingReturnInConditionalStatements ?? includingReturnStatements, includingReturnInConditionalStatements: includingReturnInConditionalStatements ) } let endOfStatement = conditionalBranches.last?.endOfBranch ?? firstTokenInBody let isOnlyStatement = index(of: .nonSpaceOrCommentOrLinebreak, after: endOfStatement) == endOfScopeIndex return isSupportedSingleStatement && isOnlyStatement } guard let expressionRange = parseExpressionRange(startingAt: firstTokenInBody), let nextIndexAfterExpression = index(of: .nonSpaceOrCommentOrLinebreak, after: expressionRange.upperBound) else { return false } return nextIndexAfterExpression == endOfScopeIndex } /// The token before the body of the scope following the given `startOfScopeIndex`. /// If this is a closure, the body starts after any `in` clause that may exist. func startOfBody(atStartOfScope startOfScopeIndex: Int) -> Int { // If this is a closure that has an `in` clause, the body scope starts after that if isStartOfClosure(at: startOfScopeIndex), let inKeywordIndex = parseClosureArgumentList(at: startOfScopeIndex)?.inKeywordIndex { return inKeywordIndex } else { return startOfScopeIndex } } typealias ConditionalBranch = (startOfBranch: Int, endOfBranch: Int) /// If `index` is the start of an `if` or `switch` statement, /// finds and returns all of the statement branches. func conditionalBranches(at index: Int) -> [ConditionalBranch]? { switch tokens[index] { case .keyword("await"): // Skip over any `try`, `try?`, `try!`, or `await` token, // which are valid before an if/switch expression. if let nextToken = self.index(of: .nonSpaceOrCommentOrLinebreak, after: index) { return conditionalBranches(at: nextToken) } return nil case .keyword("try"): if let nextIndex = self.index(of: .nonSpaceOrCommentOrLinebreak, after: index) { if tokens[nextIndex].isUnwrapOperator, let tokenAfterOperator = self.index(of: .nonSpaceOrCommentOrLinebreak, after: nextIndex) { return conditionalBranches(at: tokenAfterOperator) } else { return conditionalBranches(at: nextIndex) } } return nil case .keyword("if"): return ifStatementBranches(at: index) case .keyword("switch"): return switchStatementBranches(at: index) default: return nil } } /// Finds all of the branch bodies in an if statement. /// Returns the index of the `startOfScope` and `endOfScope` of each branch. func ifStatementBranches(at ifIndex: Int) -> [ConditionalBranch] { assert(tokens[ifIndex] == .keyword("if")) var branches = [(startOfBranch: Int, endOfBranch: Int)]() var nextConditionalBranchIndex: Int? = ifIndex while let conditionalBranchIndex = nextConditionalBranchIndex, conditionalBranchIndex == ifIndex || tokens[conditionalBranchIndex] == .keyword("else"), let startOfBody = startOfConditionalBranchBody(after: conditionalBranchIndex), let endOfBody = endOfScope(at: startOfBody) { branches.append((startOfBranch: startOfBody, endOfBranch: endOfBody)) if conditionalBranchIndex > ifIndex, next(.nonSpaceOrCommentOrLinebreak, after: conditionalBranchIndex) != .keyword("if") { // An `if` statement can only have one `else` branch that's not an `else if` break } nextConditionalBranchIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: endOfBody) } return branches } /// Returns the `startOfScope("{")` token index for this conditional branch func startOfConditionalBranchBody(after index: Int) -> Int? { guard let startOfBody = self.index(of: .startOfScope("{"), after: index) else { return nil } // If we find a closure, skip over it. if isStartOfClosure(at: startOfBody), let endOfClosure = endOfScope(at: startOfBody) { return startOfConditionalBranchBody(after: endOfClosure) } return startOfBody } /// Finds all of the branch bodies in a switch statement. /// Returns the index of the `startOfScope` and `endOfScope` of each branch, /// including branches that are inside `#if` conditional compilation blocks. func switchStatementBranches(at switchIndex: Int) -> [ConditionalBranch]? { assert(tokens[switchIndex] == .keyword("switch")) guard let startOfSwitchScope = index(of: .startOfScope("{"), after: switchIndex), let endOfSwitchScope = endOfScope(at: startOfSwitchScope) else { return nil } // Collect all case/default/@unknown indices in the switch body, including // those inside #if conditional compilation blocks. Use endOfScope() to skip // over nested non-#if scopes naturally rather than tracking depth manually. var caseIndices = [Int]() var index = index(of: .nonSpaceOrCommentOrLinebreak, after: startOfSwitchScope) while let i = index, i < endOfSwitchScope { let token = tokens[i] if token.isSwitchCaseOrDefault { caseIndices.append(i) // For `@unknown`, skip past the associated `case`/`default` token so it // isn't collected as a second, separate case entry. if token == .keyword("@unknown"), let next = self.index(of: .nonSpaceOrCommentOrLinebreak, after: i), tokens[next].isSwitchCaseOrDefault { index = self.index(of: .nonSpaceOrCommentOrLinebreak, after: next) } else { index = self.index(of: .nonSpaceOrCommentOrLinebreak, after: i) } } else if token == .startOfScope("{") || token == .startOfScope("(") || token == .startOfScope("[") { // Skip over nested scopes ({}, (), []) but not :, #if, or string scopes index = endOfScope(at: i).flatMap { self.index(of: .nonSpaceOrCommentOrLinebreak, after: $0) } } else { index = self.index(of: .nonSpaceOrCommentOrLinebreak, after: i) } } guard !caseIndices.isEmpty else { return nil } var branches = [(startOfBranch: Int, endOfBranch: Int)]() for (offset, caseIndex) in caseIndices.enumerated() { guard let (startOfBody, _) = parseSwitchStatementCase(caseOrDefaultIndex: caseIndex) else { return nil } let endOfBody: Int if offset + 1 < caseIndices.count { let nextCaseIndex = caseIndices[offset + 1] // If there is a #if, #else, or #elseif directive between this case body // and the next case, use that directive as the boundary so preprocessor // lines are not counted as part of this case's content. endOfBody = firstIfdefBoundary(after: startOfBody, before: nextCaseIndex) ?? nextCaseIndex } else { endOfBody = endOfSwitchScope } branches.append((startOfBranch: startOfBody, endOfBranch: endOfBody)) } return branches } /// Returns the index of the first `#if`, `#else`, `#elseif`, or `#endif` token in the /// range `(start, end)`, skipping over nested non-#if scopes. private func firstIfdefBoundary(after start: Int, before end: Int) -> Int? { var braceDepth = 0 var i = start + 1 while i < end { switch tokens[i] { case .startOfScope("{"), .startOfScope("("), .startOfScope("["): braceDepth += 1 case .endOfScope("}"), .endOfScope(")"), .endOfScope("]"): braceDepth -= 1 case .startOfScope("#if") where braceDepth == 0: // Only treat as a boundary if the #if block contains case/default // statements at brace depth 0 (belonging to the enclosing switch, // not a nested switch). Otherwise skip the entire #if…#endif block. if let result = scanIfdef(at: i) { if result.containsSwitchCase { return i } i = result.endifIndex } case .keyword("#else"), .keyword("#elseif"), .endOfScope("#endif") where braceDepth == 0: return i default: break } i += 1 } return nil } /// Scans the `#if` block at the given index to find its matching `#endif` /// and determine whether it contains `case`/`default` keywords at brace /// depth 0 (i.e. belonging to the enclosing switch, not a nested switch). /// Uses manual `#if`/`#endif` depth tracking instead of `endOfScope` since /// `endOfScope` cannot handle `case` tokens inside `#if` blocks. private func scanIfdef(at ifIndex: Int) -> (endifIndex: Int, containsSwitchCase: Bool)? { guard tokens[ifIndex] == .startOfScope("#if") else { return nil } var ifdefDepth = 1 var braceDepth = 0 var containsSwitchCase = false for i in (ifIndex + 1) ..< tokens.count { switch tokens[i] { case .startOfScope("#if"): ifdefDepth += 1 case .endOfScope("#endif"): ifdefDepth -= 1 if ifdefDepth == 0 { return (endifIndex: i, containsSwitchCase: containsSwitchCase) } case .startOfScope("{"), .startOfScope("("), .startOfScope("["): if ifdefDepth == 1 { braceDepth += 1 } case .endOfScope("}"), .endOfScope(")"), .endOfScope("]"): if ifdefDepth == 1 { braceDepth -= 1 } default: if ifdefDepth == 1, braceDepth == 0, tokens[i].isSwitchCaseOrDefault { containsSwitchCase = true } } } return nil } /// Parses the switch statement case starting at the given index, /// which should be one of: `case`, `default`, or `@unknown`. private func parseSwitchStatementCase(caseOrDefaultIndex: Int) -> (startOfBody: Int, endOfBody: Int)? { assert(tokens[caseOrDefaultIndex].isSwitchCaseOrDefault || tokens[caseOrDefaultIndex] == .keyword("@unknown")) // `@unknown` (a keyword) is handled differently from `case` and `default` (endOfScope tokens). // In this case we have `.keyword("@unknown"), .endOfScope("default"), .startOfScope(":")`. var caseOrDefaultIndex = caseOrDefaultIndex if tokens[caseOrDefaultIndex] == .keyword("@unknown") { guard let nextEndOfScope = endOfScope(at: caseOrDefaultIndex) else { return nil } caseOrDefaultIndex = nextEndOfScope } guard let startOfBody = index(of: .startOfScope(":"), after: caseOrDefaultIndex), var endOfBody = endOfScope(at: startOfBody) else { return nil } // If the next case has the `@unknown` prefix, make sure that token isn't included in the body of this branch. if let unknownKeyword = index(of: .nonSpaceOrCommentOrLinebreak, before: endOfBody), tokens[unknownKeyword] == .keyword("@unknown") { endOfBody = unknownKeyword } return (startOfBody: startOfBody, endOfBody: endOfBody) } /// In Swift 5.9, there's a bug that prevents you from writing an /// if or switch expression using an `as?` on one of the branches: /// https://github.com/apple/swift/issues/68764 /// /// if condition { /// foo as? String /// } else { /// "bar" /// } /// /// This helper returns whether or not the branch starting at the given `startOfScopeIndex` /// includes an `as?` operator, so wouldn't be permitted in a if/switch exprssion in Swift 5.9 func conditionalBranchHasUnsupportedCastOperator(startOfScopeIndex: Int) -> Bool { if options.swiftVersion == "5.9", let asIndex = index(of: .keyword("as"), after: startOfScopeIndex), let endOfScopeIndex = endOfScope(at: startOfScopeIndex), asIndex < endOfScopeIndex, next(.nonSpaceOrCommentOrLinebreak, after: asIndex)?.isUnwrapOperator == true, // Make sure the as? is at the top level, not nested in some // inner scope like a function call or closure startOfScope(at: asIndex) == startOfScopeIndex { return true } return false } /// Performs a closure for each conditional branch in the given conditional statement, /// including any recursive conditional inside an individual branch. /// Iterates backwards to support removing tokens in `handle`. func forEachRecursiveConditionalBranch( in branches: [ConditionalBranch], _ handle: (ConditionalBranch) -> Void ) { for branch in branches.reversed() { if let tokenAfterEquals = index(of: .nonSpaceOrCommentOrLinebreak, after: branch.startOfBranch), let conditionalBranches = conditionalBranches(at: tokenAfterEquals) { forEachRecursiveConditionalBranch(in: conditionalBranches, handle) } else { handle(branch) } } } /// Performs a check for each conditional branch in the given conditional statement, /// including any recursive conditional inside an individual branch func allRecursiveConditionalBranches( in branches: [ConditionalBranch], satisfy branchSatisfiesCondition: (ConditionalBranch) -> Bool ) -> Bool { var allSatisfy = true forEachRecursiveConditionalBranch(in: branches) { branch in if !branchSatisfiesCondition(branch) { allSatisfy = false } } return allSatisfy } /// Context describing the structure of a case in a switch statement struct SwitchStatementBranchWithSpacingInfo { let startOfBranchExcludingLeadingComments: Int let endOfBranchExcludingTrailingComments: Int let spansMultipleLines: Bool let isLastCase: Bool let isFollowedByBlankLine: Bool let linebreakBeforeEndOfScope: Int? let linebreakBeforeBlankLine: Int? /// Inserts a blank line at the end of the switch case func insertTrailingBlankLine(using formatter: Formatter) { guard let linebreakBeforeEndOfScope else { return } formatter.insertLinebreak(at: linebreakBeforeEndOfScope) } /// Removes the trailing blank line from the switch case if present func removeTrailingBlankLine(using formatter: Formatter) { guard let linebreakBeforeEndOfScope, let linebreakBeforeBlankLine else { return } formatter.removeTokens(in: (linebreakBeforeBlankLine + 1) ... linebreakBeforeEndOfScope) } } /// Finds all of the branch bodies in a switch statement, and derives additional information /// about the structure of each branch / case. func switchStatementBranchesWithSpacingInfo(at switchIndex: Int) -> [SwitchStatementBranchWithSpacingInfo]? { guard let switchStatementBranches = switchStatementBranches(at: switchIndex) else { return nil } return switchStatementBranches.enumerated().compactMap { caseIndex, switchCase -> SwitchStatementBranchWithSpacingInfo? in // Cases that end at a `#else` or `#elseif` boundary are in mutually-exclusive // compilation branches, where blank-line rules don't apply. if tokens[switchCase.endOfBranch] == .keyword("#else") || tokens[switchCase.endOfBranch] == .keyword("#elseif") { return nil } // Exclude any comments when considering if this is a single line or multi-line branch var startOfBranchExcludingLeadingComments = switchCase.startOfBranch while let tokenAfterStartOfScope = index(of: .nonSpace, after: startOfBranchExcludingLeadingComments), tokens[tokenAfterStartOfScope].isLinebreak, let commentAfterStartOfScope = index(of: .nonSpace, after: tokenAfterStartOfScope), tokens[commentAfterStartOfScope].isComment, let endOfComment = endOfScope(at: commentAfterStartOfScope), let tokenBeforeEndOfComment = index(of: .nonSpace, before: endOfComment) { if tokens[endOfComment].isLinebreak { startOfBranchExcludingLeadingComments = tokenBeforeEndOfComment } else { startOfBranchExcludingLeadingComments = endOfComment } } var endOfBranchExcludingTrailingComments = switchCase.endOfBranch while let tokenBeforeEndOfScope = index(of: .nonSpace, before: endOfBranchExcludingTrailingComments), tokens[tokenBeforeEndOfScope].isLinebreak, let commentBeforeEndOfScope = index(of: .nonSpace, before: tokenBeforeEndOfScope), tokens[commentBeforeEndOfScope].isComment, let startOfComment = startOfScope(at: commentBeforeEndOfScope), tokens[startOfComment].isComment { endOfBranchExcludingTrailingComments = startOfComment } guard let firstTokenInBody = index(of: .nonSpaceOrLinebreak, after: startOfBranchExcludingLeadingComments), let lastTokenInBody = index(of: .nonSpaceOrLinebreak, before: endOfBranchExcludingTrailingComments) else { return nil } let isLastCase = caseIndex == switchStatementBranches.indices.last let spansMultipleLines = !onSameLine(firstTokenInBody, lastTokenInBody) var isFollowedByBlankLine = false var linebreakBeforeEndOfScope: Int? var linebreakBeforeBlankLine: Int? // If the case body is bounded by `#endif`, the blank-line separator lives // *after* the `#endif` line rather than before it. Use the next non-whitespace // token after `#endif` as the reference point for blank-line detection so that // an existing blank line between `#endif` and the next case is correctly // identified, and a missing one can be inserted/removed in the right place. let endForBlankLineDetection: Int if tokens[switchCase.endOfBranch] == .endOfScope("#endif"), let tokenAfterEndif = index(of: .nonSpaceOrCommentOrLinebreak, after: switchCase.endOfBranch) { endForBlankLineDetection = tokenAfterEndif } else { endForBlankLineDetection = endOfBranchExcludingTrailingComments } if let tokenBeforeEndOfScope = index(of: .nonSpace, before: endForBlankLineDetection), tokens[tokenBeforeEndOfScope].isLinebreak { linebreakBeforeEndOfScope = tokenBeforeEndOfScope } if let linebreakBeforeEndOfScope, let tokenBeforeBlankLine = index(of: .nonSpace, before: linebreakBeforeEndOfScope), tokens[tokenBeforeBlankLine].isLinebreak { linebreakBeforeBlankLine = tokenBeforeBlankLine isFollowedByBlankLine = true } return SwitchStatementBranchWithSpacingInfo( startOfBranchExcludingLeadingComments: startOfBranchExcludingLeadingComments, endOfBranchExcludingTrailingComments: endOfBranchExcludingTrailingComments, spansMultipleLines: spansMultipleLines, isLastCase: isLastCase, isFollowedByBlankLine: isFollowedByBlankLine, linebreakBeforeEndOfScope: linebreakBeforeEndOfScope, linebreakBeforeBlankLine: linebreakBeforeBlankLine ) } } /// Whether the given index is in a function call (not declaration) func isFunctionCall(at index: Int) -> Bool { if let openingParenIndex = self.index(of: .startOfScope("("), before: index + 1) { if let prevTokenIndex = self.index(of: .nonSpaceOrCommentOrLinebreak, before: openingParenIndex), tokens[prevTokenIndex].isIdentifier { if let keywordIndex = self.index(of: .nonSpaceOrCommentOrLinebreak, before: prevTokenIndex), tokens[keywordIndex] == .keyword("func") || tokens[keywordIndex] == .keyword("init") { return false } return true } } return false } /// Whether a give else is part of a guard statement func isGuardElse(at index: Int) -> Bool { guard tokens[index] == .keyword("else"), let previousIndex = self.index(of: .nonSpaceOrCommentOrLinebreak, before: index) else { return false } guard tokens[previousIndex] == .endOfScope("}"), let startOfScope = startOfScope(at: previousIndex), lastSignificantKeyword(at: startOfScope - 1, excluding: [ "as", "is", "try", "var", "let", "case", ]) == "if" else { return true } return false } /// Whether the given index is directly within the body of the given scope, or part of a nested closure func indexIsWithinNestedClosure(_ index: Int, startOfScopeIndex: Int) -> Bool { let startOfScopeAtIndex: Int if token(at: index)?.isStartOfScope == true { startOfScopeAtIndex = index } else if let previousStartOfScope = self.index(of: .startOfScope, before: index) { startOfScopeAtIndex = previousStartOfScope } else { return false } if startOfScopeAtIndex <= startOfScopeIndex { return false } if isStartOfClosureOrFunctionBody(at: startOfScopeAtIndex) { return startOfScopeAtIndex != startOfScopeIndex } else if token(at: startOfScopeAtIndex)?.isStartOfScope == true { return indexIsWithinNestedClosure(startOfScopeAtIndex - 1, startOfScopeIndex: startOfScopeIndex) } else { return false } } func isStartOfClosureOrFunctionBody(at startOfScopeIndex: Int) -> Bool { guard tokens[startOfScopeIndex] == .startOfScope("{") else { return false } // An open brace is always one of: // - a statement with a keyword, like `if x { ...` // - a declaration with keyword, like `func x() { ... ` or `var x: String { ...` // - a closure, which won't have an associated keyword, like `value.map { ...`. if isStartOfClosure(at: startOfScopeIndex) { return true } else { // Since this isn't a closure, it should be either the start of a statement // like `if x { ...` _or_ a declaration like `func x() { ... `. // All of these cases have a keyword, so the last significant keyword // should be part of the same declaration / statement as the brace itself. return last(.keyword, before: startOfScopeIndex) == .keyword("func") } } /// Whether or not the length of the type at the given index exceeds the minimum threshold to be organized func typeLengthExceedsOrganizationThreshold(at typeKeywordIndex: Int) -> Bool { let organizationThreshold: Int switch tokens[typeKeywordIndex].string { case "class", "actor": organizationThreshold = options.organizeClassThreshold case "struct": organizationThreshold = options.organizeStructThreshold case "enum": organizationThreshold = options.organizeEnumThreshold case "extension": organizationThreshold = options.organizeExtensionThreshold default: organizationThreshold = 0 } guard organizationThreshold != 0, let startOfScope = index(of: .startOfScope("{"), after: typeKeywordIndex), let endOfScope = endOfScope(at: startOfScope) else { return true } let lineCount = tokens[startOfScope ... endOfScope] .filter(\.isLinebreak) .count - 1 return lineCount >= organizationThreshold } /// Removes any "test" prefix from the given method name func removeTestPrefix(fromFunctionAt funcKeywordIndex: Int) { // The name of a function always immediately follows the `func` keyword guard let methodNameIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: funcKeywordIndex), tokens[methodNameIndex].isIdentifier else { return } let methodName = tokens[methodNameIndex].string // Handle names like `func testFeature()` or `func test_feature()` if methodName.hasPrefix("test"), methodName != "test" { var newMethodName = String(methodName.dropFirst("test".count)) newMethodName = newMethodName.first!.lowercased() + newMethodName.dropFirst() // Handle methods like `test_feature()`, which should be updated to `feature()` rather than `_feature()`. while newMethodName.hasPrefix("_") { newMethodName = String(newMethodName.dropFirst()) } updateDeclarationName(forDeclarationAt: funcKeywordIndex, to: newMethodName) } // Handle names like ``func `test feature`()``, ``func `Test Feature`()`` if methodName.lowercased().hasPrefix("`test "), methodName.lowercased() != "`test `" { var newMethodName = String(methodName.dropFirst("`test ".count)) newMethodName = String(newMethodName.first!) + newMethodName.dropFirst() newMethodName = "`" + newMethodName updateDeclarationName(forDeclarationAt: funcKeywordIndex, to: newMethodName) } } /// Updates the name of the given declaration (function or type), unless that change could cause a build failure. func updateDeclarationName(forDeclarationAt keywordIndex: Int, to newName: String) { // The name of a declaration always immediately follows the keyword (e.g. `func`, `struct`, etc.) guard let nameIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: keywordIndex), tokens[nameIndex].isIdentifier else { return } // For type declarations (struct, class, actor, enum), don't rename if the current // name is referenced elsewhere in the file, since renaming would break those references. if case let .keyword(keyword) = tokens[keywordIndex], Token.swiftTypeKeywords.contains(keyword) { let currentIdentifier = tokens[nameIndex] let referenceCount = tokens.filter { $0 == currentIdentifier }.count guard referenceCount <= 1 else { return } } // Ensure that the new identifier is valid (e.g. starts with a letter, not a number), // and is unique / doesn't already exist somewhere in the file. let unescapedName = newName.hasPrefix("`") && newName.hasSuffix("`") ? String(newName.dropFirst().dropLast()) : newName guard !newName.isEmpty, newName.first?.isLetter == true || newName.first == "`", !tokens.contains(.identifier(newName)), !tokens.contains(.identifier(unescapedName)), !swiftKeywords.union(["Any", "Self", "self", "super", "nil", "true", "false"]).contains(unescapedName) else { return } replaceToken(at: nameIndex, with: .identifier(newName)) } } extension Formatter { /// A generic type parameter for a method final class GenericType { /// The name of the generic parameter. For example with `` the generic parameter `name` is `T`. let name: String /// The source range within angle brackets where the generic parameter is defined let definitionSourceRange: ClosedRange /// Conformances and constraints applied to this generic parameter var conformances: [GenericConformance] /// Whether or not this generic parameter can be removed and replaced with an opaque generic parameter var eligibleToRemove = true /// A constraint or conformance that applies to a generic type struct GenericConformance: Hashable { enum ConformanceType { /// A protocol constraint like `T: Fooable` case protocolConstraint /// A concrete type like `T == Foo` case concreteType } /// The name of the type being used in the constraint. For example with `T: Fooable` /// the constraint name is `Fooable` let name: String /// The name of the type being constrained. For example with `T: Fooable` the /// `typeName` is `T`. This can correspond exactly to the `name` of a `GenericType`, /// but can also be something like `T.AssociatedType` where `T` is the `name` of a `GenericType`. let typeName: String /// The type of conformance or constraint represented by this value. let type: ConformanceType /// The source range in the angle brackets or where clause where this conformance is defined. let sourceRange: ClosedRange } init(name: String, definitionSourceRange: ClosedRange, conformances: [GenericConformance] = []) { self.name = name self.definitionSourceRange = definitionSourceRange self.conformances = conformances } /// The opaque parameter syntax that represents this generic type, /// if the constraints can be expressed using this syntax func asOpaqueParameter(useSomeAny: Bool) -> [Token]? { // Protocols with primary associated types that can be used with // opaque parameter syntax. In the future we could make this extensible // so users can add their own types here. let knownProtocolsWithAssociatedTypes: [(name: String, primaryAssociatedType: String)] = [ (name: "Collection", primaryAssociatedType: "Element"), (name: "Sequence", primaryAssociatedType: "Element"), ] let constraints = conformances.filter { $0.type == .protocolConstraint } let concreteTypes = conformances.filter { $0.type == .concreteType } // If we have no type requirements at all, this is an // unconstrained generic and is equivalent to `some Any` if constraints.isEmpty, concreteTypes.isEmpty { guard useSomeAny else { return nil } return tokenize("some Any") } if constraints.isEmpty { // If we have no constraints but exactly one concrete type (e.g. `== String`) // then we can just substitute for that type. This sort of generic same-type // requirement (`func foo(_ t: T) where T == Foo`) is actually no longer // allowed in Swift 6, since it's redundant. if concreteTypes.count == 1 { return tokenize(concreteTypes[0].name) } // If there are multiple same-type type requirements, // the code should fail to compile else { return nil } } var primaryAssociatedTypes = [GenericConformance: GenericConformance]() // Validate that all of the conformances can be represented using this syntax for conformance in conformances { if conformance.typeName.contains(".") { switch conformance.type { case .protocolConstraint: // Constraints like `Foo.Bar: Barable` cannot be represented using // opaque generic parameter syntax return nil case .concreteType: // Concrete type constraints like `Foo.Element == Bar` can be // represented using opaque generic parameter syntax if we know // that it's using a primary associated type of the base protocol // (e.g. if `Foo` is a `Collection` or `Sequence`) let typeElements = conformance.typeName.components(separatedBy: ".") guard typeElements.count == 2 else { return nil } let associatedTypeName = typeElements[1] // Look up if the generic param conforms to any of the protocols // with a primary associated type matching the one we found let matchingProtocolWithAssociatedType = constraints.first(where: { genericConstraint in let knownProtocol = knownProtocolsWithAssociatedTypes.first(where: { $0.name == genericConstraint.name }) return knownProtocol?.primaryAssociatedType == associatedTypeName }) if let matchingProtocolWithAssociatedType { primaryAssociatedTypes[matchingProtocolWithAssociatedType] = conformance } else { // If this isn't the primary associated type of a protocol constraint, then we can't use it return nil } } } } let constraintRepresentations = constraints.map { constraint -> String in if let primaryAssociatedType = primaryAssociatedTypes[constraint] { return "\(constraint.name)<\(primaryAssociatedType.name)>" } else { return constraint.name } } return tokenize("some \(constraintRepresentations.joined(separator: " & "))") } } /// Parses generic types between the angle brackets of a function declaration, or in a where clause func parseGenericTypes( from genericSignatureStartIndex: Int ) -> (types: [GenericType], range: ClosedRange) { var types = [GenericType]() let range = parseGenericTypes(from: genericSignatureStartIndex, into: &types) return (types, range) } /// Parses generic types between the angle brackets of a function declaration, or in a where clause @discardableResult func parseGenericTypes( from genericSignatureStartIndex: Int, into genericTypes: inout [GenericType], qualifyGenericTypeName: (String) -> String = { $0 } ) -> ClosedRange { assert([.startOfScope("<"), .keyword("where")].contains(tokens[genericSignatureStartIndex])) var currentIndex = genericSignatureStartIndex while currentIndex < tokens.count { guard let lhsTypeIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: currentIndex), let lhsType = parseType(at: lhsTypeIndex) else { break } currentIndex = lhsType.range.upperBound // Parse the constraint after the type name if present var conformanceType: GenericType.GenericConformance.ConformanceType? // This can either be a protocol constraint of the form `T: Fooable` if let colonIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: currentIndex), tokens[colonIndex] == .delimiter(":") { conformanceType = .protocolConstraint currentIndex = colonIndex } // or a concrete type of the form `T == Foo` else if let equalsIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: currentIndex), tokens[equalsIndex].isOperator, tokens[equalsIndex].string == "==" { conformanceType = .concreteType currentIndex = equalsIndex } var rhsType: TypeName? if let rhsTypeIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: currentIndex), let type = parseType(at: rhsTypeIndex) { rhsType = type currentIndex = type.range.upperBound } // The generic clause can continue with a comma. let hasMoreElements: Bool if let commaIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: currentIndex), tokens[commaIndex] == .delimiter(",") { currentIndex = commaIndex hasMoreElements = true // Include any trailing spaces, comments, or newlines with this type. if let nextToken = index(of: .nonSpaceOrCommentOrLinebreak, after: currentIndex) { currentIndex = nextToken - 1 } } else { // Otherwise this is the last element in the list. hasMoreElements = false // Include any trailing spaces or comments with this type. // Don't include any newlines, since in the case of a protocol definition // this could be the last time in the entire declaration. if let nextToken = index(of: .nonSpaceOrComment, after: currentIndex) { currentIndex = nextToken - 1 } } // The generic constraint could have syntax like `Foo`, `Foo: Fooable`, // `Foo.Element == Fooable`, etc. Create a reference to this specific // generic parameter (`Foo` in all of these examples) that can store // the constraints and conformances that we encounter later. let fullGenericTypeName = qualifyGenericTypeName(lhsType.string) let baseGenericTypeName: String if fullGenericTypeName.contains(".") { baseGenericTypeName = fullGenericTypeName.components(separatedBy: ".")[0] } else { baseGenericTypeName = fullGenericTypeName } let genericType: GenericType if let existingType = genericTypes.first(where: { $0.name == baseGenericTypeName }) { genericType = existingType } else { genericType = GenericType( name: baseGenericTypeName, definitionSourceRange: lhsType.range.lowerBound ... currentIndex ) genericTypes.append(genericType) } if let rhsType, let conformanceType { genericType.conformances.append(.init( name: rhsType.string, typeName: qualifyGenericTypeName(lhsType.string), type: conformanceType, sourceRange: lhsType.range.lowerBound ... currentIndex )) } if !hasMoreElements { break } } if tokens[genericSignatureStartIndex] == .startOfScope("<"), let endOfScope = endOfScope(at: genericSignatureStartIndex) { return genericSignatureStartIndex ... endOfScope } else { // where clauses don't have an explicit end token, so end at the last index of the final element return genericSignatureStartIndex ... currentIndex } } /// Add or remove self or Self func addOrRemoveSelf(static staticSelf: Bool) { let selfKeyword = staticSelf ? "Self" : "self" // Must be applied to the entire file to work reliably guard !options.fragment else { return } func processBody(at index: inout Int, localNames: Set, members: Set, typeStack: inout [(name: String, keyword: String)], closureStack: inout [(allowsImplicitSelf: Bool, selfCapture: String?)], membersByType: inout [String: Set], classMembersByType: inout [String: Set], usingDynamicLookup: Bool, classOrStatic: Bool, isTypeRoot: Bool, isInit: Bool) { var explicitSelf: SelfMode { staticSelf ? .remove : options.explicitSelf } let isWhereClause = index > 0 && tokens[index - 1] == .keyword("where") assert(isWhereClause || currentScope(at: index).map { token -> Bool in [.startOfScope("{"), .startOfScope(":"), .startOfScope("#if")].contains(token) } ?? true) let isCaseClause = !isWhereClause && index > 0 && tokens[index - 1].isSwitchCaseOrDefault if explicitSelf == .remove { // Check if scope actually includes self before we waste a bunch of time var scopeStack: [Token] = [] loop: for i in index ..< tokens.count { let token = tokens[i] switch token { case .identifier(selfKeyword): break loop // Contains self case .startOfScope("{") where isWhereClause && scopeStack.isEmpty: return // Does not contain self case .startOfScope("{"), .startOfScope("("), .startOfScope("["), .startOfScope(":"): scopeStack.append(token) case .endOfScope("}"), .endOfScope(")"), .endOfScope("]"), .endOfScope("case"), .endOfScope("default"): if scopeStack.isEmpty || (scopeStack == [.startOfScope(":")] && isCaseClause) { index = i + 1 return // Does not contain self } _ = scopeStack.popLast() default: break } } } let inClosureDisallowingImplicitSelf = !staticSelf && closureStack.last?.allowsImplicitSelf == false // Starting in Swift 5.8, self can be rebound using a `let self = self` unwrap condition // within a weak self closure. This is the only place where defining a property // named self affects the behavior of implicit self. let scopeAllowsImplicitSelfRebinding = !staticSelf && options.swiftVersion >= "5.8" && closureStack.last?.selfCapture == "weak self" // Gather members & local variables let type = (isTypeRoot && typeStack.count == 1) ? typeStack.first : nil var members = (type?.name).flatMap { membersByType[$0] } ?? members var classMembers = (type?.name).flatMap { classMembersByType[$0] } ?? Set() let inputLocalNames = localNames var localNames = localNames if !isTypeRoot || explicitSelf != .remove { var i = index var classOrStatic = false outer: while let token = token(at: i) { switch token { case .keyword("import"): guard let nextIndex = self.index(of: .identifier, after: i) else { return fatalError("Expected identifier", at: i) } i = nextIndex case .keyword("class"), .keyword("static"): classOrStatic = true case .keyword("repeat"): if next(.nonSpaceOrCommentOrLinebreak, after: i) == .startOfScope("{") { guard let nextIndex = self.index(of: .keyword("while"), after: i) else { return fatalError("Expected while", at: i) } i = nextIndex } else { // Probably a parameter pack break } case .keyword("if"), .keyword("for"), .keyword("while"): if explicitSelf == .insert { break } guard let nextIndex = self.index(of: .startOfScope("{"), after: i) else { return fatalError("Expected {", at: i) } i = nextIndex continue case .keyword("switch"): guard let nextIndex = self.index(of: .startOfScope("{"), after: i) else { return fatalError("Expected {", at: i) } guard var endIndex = self.index(of: .endOfScope, after: nextIndex) else { return fatalError("Expected }", at: i) } while tokens[endIndex] != .endOfScope("}") { guard let nextIndex = self.index(of: .startOfScope(":"), after: endIndex) else { return fatalError("Expected :", at: i) } guard let _endIndex = self.index(of: .endOfScope, after: nextIndex) else { return fatalError("Expected end of scope", at: i) } endIndex = _endIndex } i = endIndex case .keyword("var"), .keyword("let"): i += 1 if isTypeRoot { if classOrStatic { processDeclaredVariables(at: &i, names: &classMembers) classOrStatic = false } else { processDeclaredVariables(at: &i, names: &members) } } else { let removeSelf = explicitSelf != .insert && !usingDynamicLookup && ( (staticSelf && classOrStatic) || (!staticSelf && !inClosureDisallowingImplicitSelf) ) processDeclaredVariables(at: &i, names: &localNames, removeSelfKeyword: removeSelf ? selfKeyword : nil, onlyLocal: options.swiftVersion < "5", scopeAllowsImplicitSelfRebinding: scopeAllowsImplicitSelfRebinding) } case .keyword("func"): guard let nameToken = next(.nonSpaceOrCommentOrLinebreak, after: i) else { break } if isTypeRoot { if classOrStatic { classMembers.insert(nameToken.unescaped()) classOrStatic = false } else { members.insert(nameToken.unescaped()) } } else { localNames.insert(nameToken.unescaped()) } case .startOfScope("("), .startOfScope("#if"), .startOfScope(":"), .startOfScope("/*"), .startOfScope("//"): break case .startOfScope: classOrStatic = false i = endOfScope(at: i) ?? (tokens.count - 1) case .endOfScope("}"), .endOfScope("case"), .endOfScope("default"): break outer case .endOfScope("*/"), .linebreak: updateEnablement(at: i) default: break } i += 1 } } if let type { membersByType[type.name] = members classMembersByType[type.name] = classMembers } // Remove or add `self` var lastKeyword = "" var lastKeywordIndex = 0 var classOrStatic = classOrStatic var scopeStack = [(token: Token.space(""), dynamicMemberTypes: Set())] // TODO: restructure this to use forEachToken to avoid exposing comment directives mechanism while let token = token(at: index) { switch token { case .keyword("is"), .keyword("as"), .keyword("try"), .keyword("await"): break case .keyword("init"), .keyword("subscript"), .keyword("func") where lastKeyword != "import": lastKeyword = "" let members = classOrStatic ? classMembers : members // For the staticSelf (redundantStaticSelf) rule, nested functions are not // within the static func scope, so Self. should be preserved inside them. let effectiveClassOrStatic = staticSelf && !isTypeRoot ? false : classOrStatic processFunction(at: &index, localNames: localNames, members: members, typeStack: &typeStack, closureStack: &closureStack, membersByType: &membersByType, classMembersByType: &classMembersByType, usingDynamicLookup: usingDynamicLookup, classOrStatic: effectiveClassOrStatic) classOrStatic = false continue case .keyword("static"): if !isTypeRoot { return fatalError("The static modifier is not valid outside a type body", at: index) } classOrStatic = true case .keyword("class") where next(.nonSpaceOrCommentOrLinebreak, after: index)?.isIdentifier == false: if last(.nonSpaceOrCommentOrLinebreak, before: index) != .delimiter(":") { if !isTypeRoot { return fatalError("The class modifier is not valid outside a type body", at: index) } classOrStatic = true } case .keyword("where") where lastKeyword == "protocol", .keyword("protocol"): if let startIndex = self.index(of: .startOfScope("{"), after: index), let endIndex = endOfScope(at: startIndex) { index = endIndex } case .keyword("extension"), .keyword("struct"), .keyword("enum"), .keyword("class"), .keyword("actor"), .keyword("where") where ["extension", "struct", "enum", "class", "actor"].contains(lastKeyword): let keyword = tokens[index].string guard last(.nonSpaceOrCommentOrLinebreak, before: index) != .keyword("import") else { break } guard let scopeStart = self.index(of: .startOfScope("{"), after: index), case let .identifier(name)? = next(.identifier, after: index) else { return } var usingDynamicLookup = modifiersForDeclaration( at: index, contains: "@dynamicMemberLookup" ) if usingDynamicLookup { scopeStack[scopeStack.count - 1].dynamicMemberTypes.insert(name) } else if [token.string, lastKeyword].contains("extension"), scopeStack.last!.dynamicMemberTypes.contains(name) { usingDynamicLookup = true } index = scopeStart + 1 typeStack.append((name: name, keyword: keyword)) processBody(at: &index, localNames: ["init"], members: [], typeStack: &typeStack, closureStack: &closureStack, membersByType: &membersByType, classMembersByType: &classMembersByType, usingDynamicLookup: usingDynamicLookup, classOrStatic: classOrStatic, isTypeRoot: true, isInit: false) index -= 1 typeStack.removeLast() case .keyword("case") where ["if", "while", "guard", "for"].contains(lastKeyword): break case .keyword("var"), .keyword("let"): lastKeywordIndex = index index += 1 switch lastKeyword { case "lazy" where options.swiftVersion < "4": loop: while let nextIndex = self.index(of: .nonSpaceOrCommentOrLinebreak, after: index) { switch tokens[nextIndex] { case .keyword("as"), .keyword("is"), .keyword("try"), .keyword("await"): break case .keyword, .startOfScope("{"): break loop default: break } index = nextIndex } lastKeyword = "" case "if", "while", "guard", "for": // Guard is included because it's an error to reference guard vars in body var scopedNames = localNames let removeSelf = explicitSelf != .insert && !usingDynamicLookup && ( (staticSelf && classOrStatic) || (!staticSelf && !inClosureDisallowingImplicitSelf) ) // For guard, collect declared variable names separately // so we can exclude them from the else body's scope var guardDeclaredNames = Set() if lastKeyword == "guard" { var tempIndex = index processDeclaredVariables( at: &tempIndex, names: &guardDeclaredNames, removeSelfKeyword: nil, onlyLocal: false, scopeAllowsImplicitSelfRebinding: false ) } processDeclaredVariables( at: &index, names: &scopedNames, removeSelfKeyword: removeSelf ? selfKeyword : nil, onlyLocal: false, scopeAllowsImplicitSelfRebinding: scopeAllowsImplicitSelfRebinding ) while let scope = currentScope(at: index) ?? self.token(at: index), case let .startOfScope(name) = scope, ["[", "("].contains(name) || scope.isStringDelimiter, let endIndex = endOfScope(at: index) { // TODO: find less hacky workaround index = endIndex + 1 } while scopeStack.last?.token == .startOfScope("(") { scopeStack.removeLast() } guard var startIndex = self.token(at: index) == .startOfScope("{") ? index : self.index(of: .startOfScope("{"), after: index) else { return fatalError("Expected {", at: index) } while isStartOfClosure(at: startIndex) { guard let i = self.index(of: .endOfScope("}"), after: startIndex) else { return fatalError("Expected }", at: startIndex) } guard let j = self.index(of: .startOfScope("{"), after: i) else { return fatalError("Expected {", at: i) } startIndex = j } index = startIndex + 1 // For guard, the body is the else block where guard vars are not in scope // (but pre-existing locals like function params remain in scope) let bodyLocalNames = (lastKeyword == "guard") ? localNames.subtracting(guardDeclaredNames.subtracting(inputLocalNames)) : scopedNames processBody(at: &index, localNames: bodyLocalNames, members: members, typeStack: &typeStack, closureStack: &closureStack, membersByType: &membersByType, classMembersByType: &classMembersByType, usingDynamicLookup: usingDynamicLookup, classOrStatic: classOrStatic, isTypeRoot: false, isInit: isInit) index -= 1 lastKeyword = "" default: lastKeyword = token.string } classOrStatic = false case .keyword("where") where lastKeyword == "in", .startOfScope("{") where lastKeyword == "in" && !isStartOfClosure(at: index): lastKeyword = "" var localNames = localNames guard let keywordIndex = self.index(of: .keyword("in"), before: index), let prevKeywordIndex = self.index(of: .keyword("for"), before: keywordIndex) else { return fatalError("Expected for keyword", at: index) } for token in tokens[prevKeywordIndex + 1 ..< keywordIndex] { if case let .identifier(name) = token, name != "_" { localNames.insert(token.unescaped()) } } index += 1 processBody(at: &index, localNames: localNames, members: members, typeStack: &typeStack, closureStack: &closureStack, membersByType: &membersByType, classMembersByType: &classMembersByType, usingDynamicLookup: usingDynamicLookup, classOrStatic: classOrStatic, isTypeRoot: false, isInit: isInit) continue case .keyword("while") where lastKeyword == "repeat": lastKeyword = "" case .startOfScope("#if"), .keyword("#elseif"): // Skip the condition to avoid treating compiler directive // arguments (e.g., `os(iOS)`) as property references if case .startOfScope = token { scopeStack.append((token, [])) } if let linebreakIndex = self.index(of: .linebreak, after: index) { index = linebreakIndex } case let .keyword(name) where !name.isMacro: lastKeyword = name lastKeywordIndex = index case .startOfScope("/*"), .startOfScope("//"): index = endOfScope(at: index) ?? (tokens.count - 1) updateEnablement(at: index) case .startOfScope where token.isStringDelimiter, .startOfScope("["), .startOfScope("("): scopeStack.append((token, [])) case .startOfScope(":"): lastKeyword = "" case .startOfScope("{") where lastKeyword == "catch": lastKeyword = "" var localNames = localNames localNames.insert("error") // Implicit error argument index += 1 processBody(at: &index, localNames: localNames, members: members, typeStack: &typeStack, closureStack: &closureStack, membersByType: &membersByType, classMembersByType: &classMembersByType, usingDynamicLookup: usingDynamicLookup, classOrStatic: classOrStatic, isTypeRoot: false, isInit: isInit) continue case .startOfScope("{") where isWhereClause && scopeStack.count == 1: return case .startOfScope("{") where lastKeyword == "switch" && scopeStack.count == 1: lastKeyword = "" index += 1 loop: while let token = self.token(at: index) { index += 1 switch token { case .endOfScope("case"), .endOfScope("default"): let localNames = localNames processBody(at: &index, localNames: localNames, members: members, typeStack: &typeStack, closureStack: &closureStack, membersByType: &membersByType, classMembersByType: &classMembersByType, usingDynamicLookup: usingDynamicLookup, classOrStatic: classOrStatic, isTypeRoot: false, isInit: isInit) index -= 1 case .endOfScope("}"): break loop default: break } } case .startOfScope("{") where ["for", "where", "if", "else", "while", "do"].contains(lastKeyword): if let scopeIndex = self.index(of: .startOfScope, before: index), scopeIndex > lastKeywordIndex { index = endOfScope(at: index) ?? (tokens.count - 1) break } lastKeyword = "" fallthrough case .startOfScope("{") where lastKeyword == "repeat": index += 1 processBody(at: &index, localNames: localNames, members: members, typeStack: &typeStack, closureStack: &closureStack, membersByType: &membersByType, classMembersByType: &classMembersByType, usingDynamicLookup: usingDynamicLookup, classOrStatic: classOrStatic, isTypeRoot: false, isInit: isInit) continue case .startOfScope("{") where lastKeyword == "var": lastKeyword = "" if isStartOfClosure(at: index) { fallthrough } var prevIndex = index - 1 var name: String? while let token = self.token(at: prevIndex), token != .keyword("var") { if case let .identifier(_name) = token { // Is the declared variable name = _name } prevIndex -= 1 } let classOrStatic = modifiersForDeclaration(at: lastKeywordIndex, contains: { _, string in ["static", "class"].contains(string) }) if let name, classOrStatic || !staticSelf { processAccessors(["get", "set", "willSet", "didSet", "init", "_modify"], for: name, at: &index, localNames: localNames, members: members, typeStack: &typeStack, closureStack: &closureStack, membersByType: &membersByType, classMembersByType: &classMembersByType, usingDynamicLookup: usingDynamicLookup, classOrStatic: classOrStatic) } else { index = (endOfScope(at: index) ?? index) + 1 } continue case .startOfScope("{") where isStartOfClosure(at: index): let inIndex = self.index(of: .keyword, after: index, if: { $0 == .keyword("in") }) // Parse the capture list and arguments list, // and record the type of `self` capture used in the closure var captureList: [Token]? var parameterList: [Token]? // Handle a capture list followed by an optional parameter list: // `{ [self, foo] bar in` or `{ [self, foo] in` etc. if let inIndex, let captureListStartIndex = self.index(in: (index + 1) ..< inIndex, where: { !$0.isSpaceOrCommentOrLinebreak && !$0.isAttribute }), tokens[captureListStartIndex] == .startOfScope("["), let captureListEndIndex = endOfScope(at: captureListStartIndex) { captureList = Array(tokens[(captureListStartIndex + 1) ..< captureListEndIndex]) parameterList = Array(tokens[(captureListEndIndex + 1) ..< inIndex]) } // Handle a parameter list if present without a capture list // e.g. `{ foo, bar in` else if let inIndex, let firstTokenInClosure = self.index(of: .nonSpaceOrCommentOrLinebreak, after: index), isInClosureArguments(at: firstTokenInClosure) { parameterList = Array(tokens[firstTokenInClosure ..< inIndex]) } var captureListEntries = (captureList ?? []).split(separator: .delimiter(","), omittingEmptySubsequences: true) let parameterListEntries = (parameterList ?? []).split(separator: .delimiter(","), omittingEmptySubsequences: true) let supportedSelfCaptures = Set([ "self", "unowned self", "unowned(safe) self", "unowned(unsafe) self", "weak self", ]) let captureEntryStrings = captureListEntries.map { captureListEntry in captureListEntry .map(\.string) .joined() .trimmingCharacters(in: .whitespacesAndNewlines) } let selfCapture = captureEntryStrings.first(where: { supportedSelfCaptures.contains($0) }) captureListEntries.removeAll(where: { captureListEntry in let text = captureListEntry .map(\.string) .joined() .trimmingCharacters(in: .whitespacesAndNewlines) return text == selfCapture }) let localDefiningDeclarations = captureListEntries + parameterListEntries var closureLocalNames = localNames for tokens in localDefiningDeclarations { guard let localIdentifier = tokens.first(where: { $0.isIdentifier && !_FormatRules.ownershipModifiers.contains($0.string) }), case let .identifier(name) = localIdentifier, name != "_" else { continue } closureLocalNames.insert(name) } // Functions defined inside closures with `[weak self]` captures can // only use implicit self once self has been unwrapped. // // When using `weak self` we add `self` to locals and will remove // it again if we encounter a `guard let self`. if options.swiftVersion >= "5.8", selfCapture == "weak self" { closureLocalNames.insert("self") } /// Whether or not the closure at the current index permits implicit self. /// /// SE-0269 (in Swift 5.3) allows implicit self when: /// - the closure captures self explicitly using [self] or [unowned self] /// - self is not a reference type /// /// SE-0365 (in Swift 5.8) additionally allows implicit self using /// [weak self] captures after self has been unwrapped. func closureAllowsImplicitSelf() -> Bool { guard options.swiftVersion >= "5.3" else { return false } // If self is a reference type, capturing it won't create a retain cycle, // so the compiler lets us use implicit self if let enclosingTypeKeyword = typeStack.last?.keyword, enclosingTypeKeyword == "struct" || enclosingTypeKeyword == "enum" { return true } guard let selfCapture else { return false } // If self is captured strongly, or using `unowned`, then the compiler // lets us use implicit self since it's already clear that this closure // captures self strongly if selfCapture == "self" || selfCapture == "unowned self" || selfCapture == "unowned(safe) self" || selfCapture == "unowned(unsafe) self" { return true } // This is also supported for `weak self` captures, but only // in Swift 5.8 or later if selfCapture == "weak self", options.swiftVersion >= "5.8" { return true } return false } closureStack.append((allowsImplicitSelf: closureAllowsImplicitSelf(), selfCapture: selfCapture)) index = (inIndex ?? index) + 1 // For the staticSelf (redundantStaticSelf) rule, closures are not within // the static func scope, so Self. should be preserved inside them. processBody(at: &index, localNames: closureLocalNames, members: members, typeStack: &typeStack, closureStack: &closureStack, membersByType: &membersByType, classMembersByType: &classMembersByType, usingDynamicLookup: usingDynamicLookup, classOrStatic: staticSelf ? false : classOrStatic, isTypeRoot: false, isInit: isInit) index -= 1 closureStack.removeLast() case .startOfScope: index = endOfScope(at: index) ?? (tokens.count - 1) case .identifier(selfKeyword): guard isEnabled, explicitSelf != .insert, !isTypeRoot, !usingDynamicLookup, !staticSelf || classOrStatic, let dotIndex = self.index(of: .nonSpaceOrLinebreak, after: index, if: { $0 == .operator(".", .infix) }), let nextIndex = self.index(of: .nonSpaceOrLinebreak, after: dotIndex) else { break } if explicitSelf == .insert { break } else if explicitSelf == .initOnly, isInit { if next(.nonSpaceOrCommentOrLinebreak, after: nextIndex) == .operator("=", .infix) { break } else if let scopeEnd = self.index(of: .endOfScope(")"), after: nextIndex), next(.nonSpaceOrCommentOrLinebreak, after: scopeEnd) == .operator("=", .infix) { break } } if let closure = closureStack.last, !closure.allowsImplicitSelf { break } _ = removeSelf(at: index, exclude: localNames) case .identifier("type"): // Special case for type(of:) guard let parenIndex = self.index(of: .nonSpaceOrCommentOrLinebreak, after: index, if: { $0 == .startOfScope("(") }), next(.nonSpaceOrCommentOrLinebreak, after: parenIndex) == .identifier("of") else { fallthrough } case .keyword where token.isMacro, .identifier: guard isEnabled && !isTypeRoot else { break } if explicitSelf == .insert { // continue } else if explicitSelf == .initOnly, isInit { if next(.nonSpaceOrCommentOrLinebreak, after: index) == .operator("=", .infix) { // continue } else if let scopeEnd = self.index(of: .endOfScope(")"), after: index), next(.nonSpaceOrCommentOrLinebreak, after: scopeEnd) == .operator("=", .infix) { // continue } else { if token.string == "lazy" { lastKeyword = "lazy" lastKeywordIndex = index } break } } else { if token.string == "lazy" { lastKeyword = "lazy" lastKeywordIndex = index } break } let isAssignment: Bool if ["for", "var", "let"].contains(lastKeyword), let prevToken = last(.nonSpaceOrCommentOrLinebreak, before: index) { switch prevToken { case .identifier, .number, .endOfScope, .operator where ![ .operator("=", .infix), .operator(".", .prefix), ].contains(prevToken): isAssignment = false lastKeyword = "" default: isAssignment = true } } else { isAssignment = false } if !isAssignment, token.string == "lazy" { lastKeyword = "lazy" lastKeywordIndex = index } let name = token.unescaped() guard members.contains(name), !localNames.contains(name), !isAssignment || last(.nonSpaceOrCommentOrLinebreak, before: index) == .operator("=", .infix), next(.nonSpaceOrComment, after: index) != .delimiter(":") else { break } if let lastToken = last(.nonSpaceOrCommentOrLinebreak, before: index), lastToken.isOperator(".") { break } if lastKeyword.hasPrefix("@"), let startIndex = startOfScope(at: index), tokens[startIndex] == .startOfScope("("), lastKeywordIndex == self.index(of: .nonSpaceOrComment, before: startIndex) { break } insert([.identifier("self"), .operator(".", .infix)], at: index) index += 2 case .endOfScope("case"), .endOfScope("default"): return case .endOfScope: if token == .endOfScope("#endif") { while let scope = scopeStack.last?.token, scope != .space("") { scopeStack.removeLast() if scope != .startOfScope("#if") { break } } } else if let scope = scopeStack.last?.token, scope != .space("") { // TODO: fix this bug assert(token.isEndOfScope(scope)) scopeStack.removeLast() } else { assert(token.isEndOfScope(currentScope(at: index)!)) index += 1 return } case .linebreak: updateEnablement(at: index) default: break } index += 1 } } func processAccessors(_ names: [String], for name: String, at index: inout Int, localNames: Set, members: Set, typeStack: inout [(name: String, keyword: String)], closureStack: inout [(allowsImplicitSelf: Bool, selfCapture: String?)], membersByType: inout [String: Set], classMembersByType: inout [String: Set], usingDynamicLookup: Bool, classOrStatic: Bool) { assert(tokens[index] == .startOfScope("{")) var foundAccessors = false var localNames = localNames while var nextIndex = self.index(of: .nonSpaceOrCommentOrLinebreak, after: index, if: { switch $0 { case .keyword where $0.isAttribute: return true case let .identifier(name), let .keyword(name): return names.contains(name) default: return false } }), let startIndex = self.index(of: .startOfScope("{"), after: nextIndex) { if tokens[nextIndex].isAttribute { guard let startIndex = self.index(of: .nonSpaceOrComment, after: nextIndex), tokens[startIndex] == .startOfScope("("), let endIndex = endOfScope(at: startIndex) else { // Not an accessor break } // Could be an attribute on an accessor index = endIndex continue } foundAccessors = true index = startIndex + 1 if let parenStart = self.index(of: .nonSpaceOrCommentOrLinebreak, after: nextIndex, if: { $0 == .startOfScope("(") }), let varToken = next(.identifier, after: parenStart) { localNames.insert(varToken.unescaped()) } else { var token = tokens[nextIndex] while token.isAttribute, let endIndex = endOfAttribute(at: nextIndex), let index = self.index(of: .nonSpaceOrCommentOrLinebreak, after: endIndex) { nextIndex = index token = tokens[nextIndex] } switch token.string { case "get", "_modify": localNames.insert(name) case "set", "init": localNames.insert(name) localNames.insert("newValue") case "willSet": localNames.insert("newValue") case "didSet": localNames.insert("oldValue") default: break } } processBody(at: &index, localNames: localNames, members: members, typeStack: &typeStack, closureStack: &closureStack, membersByType: &membersByType, classMembersByType: &classMembersByType, usingDynamicLookup: usingDynamicLookup, classOrStatic: classOrStatic, isTypeRoot: false, isInit: false) index -= 1 } if foundAccessors { guard let endIndex = self.index(of: .endOfScope("}"), after: index) else { return fatalError("Expected }", at: index) } index = endIndex + 1 } else { index += 1 localNames.insert(name) processBody(at: &index, localNames: localNames, members: members, typeStack: &typeStack, closureStack: &closureStack, membersByType: &membersByType, classMembersByType: &classMembersByType, usingDynamicLookup: usingDynamicLookup, classOrStatic: classOrStatic, isTypeRoot: false, isInit: false) } } func processFunction(at index: inout Int, localNames: Set, members: Set, typeStack: inout [(name: String, keyword: String)], closureStack: inout [(allowsImplicitSelf: Bool, selfCapture: String?)], membersByType: inout [String: Set], classMembersByType: inout [String: Set], usingDynamicLookup: Bool, classOrStatic: Bool) { let funcKeywordIndex = index let startToken = tokens[index] var localNames = localNames guard let startIndex = self.index(of: .startOfScope("("), after: index), let endIndex = self.index(of: .endOfScope(")"), after: startIndex) else { index += 1 // Prevent endless loop return } // Get argument names index = startIndex while index < endIndex { guard let externalNameIndex = self.index(of: .identifier, after: index), let nextIndex = self.index(of: .nonSpaceOrCommentOrLinebreak, after: externalNameIndex) else { break } let token = tokens[nextIndex] switch token { case let .identifier(name) where name != "_": localNames.insert(token.unescaped()) case .delimiter(":"): let externalNameToken = tokens[externalNameIndex] if case let .identifier(name) = externalNameToken, name != "_" { localNames.insert(externalNameToken.unescaped()) } default: break } index = self.index(of: .delimiter(","), after: index) ?? endIndex } let bodyStartIndex: Int if let functionDeclaration = parseFunctionDeclaration(keywordIndex: funcKeywordIndex) { guard let validBodyStartIndex = functionDeclaration.bodyRange?.lowerBound else { // Ensure we move the `redundantSelf` work index to the end of the function declaration index = functionDeclaration.range.upperBound return } bodyStartIndex = validBodyStartIndex } else { // If `parseFunctionDeclaration` fails due to some unsupported pattern, use a more permissive search. guard let validBodyStartIndex = self.index(after: endIndex, where: { switch $0 { case .startOfScope("{"): // What we're looking for return true case .keyword("throws"), .keyword("rethrows"), .keyword("where"), .keyword("is"), .keyword("repeat"): return false // Keep looking case .keyword where !$0.isAttribute: return true // Not valid between end of arguments and start of body default: return false // Keep looking } }), tokens[validBodyStartIndex] == .startOfScope("{") else { return } bodyStartIndex = validBodyStartIndex } // Functions defined inside closures with `[weak self]` captures can // never use implicit self. // // When we encounter a `guard let self` in a `[weak self]` closure, // we remove `self` from the list of locals since that disables // implicit self. Now that we're moving into a function that doesn't // support implicit self, we can re-insert `self` into the list of // locals to disable implicit self again for this scope. if options.swiftVersion >= "5.8", closureStack.last?.selfCapture == "weak self" { localNames.insert("self") } if startToken == .keyword("subscript") { index = bodyStartIndex processAccessors(["get", "set"], for: "", at: &index, localNames: localNames, members: members, typeStack: &typeStack, closureStack: &closureStack, membersByType: &membersByType, classMembersByType: &classMembersByType, usingDynamicLookup: usingDynamicLookup, classOrStatic: classOrStatic) } else { index = bodyStartIndex + 1 processBody(at: &index, localNames: localNames, members: members, typeStack: &typeStack, closureStack: &closureStack, membersByType: &membersByType, classMembersByType: &classMembersByType, usingDynamicLookup: usingDynamicLookup, classOrStatic: classOrStatic, isTypeRoot: false, isInit: startToken == .keyword("init")) } } var typeStack = [(name: String, keyword: String)]() var closureStack = [(allowsImplicitSelf: Bool, selfCapture: String?)]() var membersByType = [String: Set]() var classMembersByType = [String: Set]() var index = 0 processBody(at: &index, localNames: [], members: [], typeStack: &typeStack, closureStack: &closureStack, membersByType: &membersByType, classMembersByType: &classMembersByType, usingDynamicLookup: false, classOrStatic: false, isTypeRoot: false, isInit: false) } /// Ensures that the given range ends with at least one trailing blank line, /// by adding a blank line to the end of this declaration if not already present. func addTrailingBlankLineIfNeeded(in range: ClosedRange) { let range = range.autoUpdating(in: self) while tokens[range].numberOfTrailingLinebreaks() < 2 { insertLinebreak(at: range.upperBound) } } /// Ensures that given range doesn't end with a trailing blank line /// by removing any trailing blank lines. func removeTrailingBlankLinesIfPresent(in range: ClosedRange) { let range = range.autoUpdating(in: self) while tokens[range.range].numberOfTrailingLinebreaks() > 1 { guard let lastNewlineIndex = lastIndex(of: .linebreak, in: Range(range.range)) else { break } removeToken(at: lastNewlineIndex) // If the removed linebreak was at the end of a blank line that had trailing // whitespace (i.e. the preceding token is a space and the one before that is // a linebreak), also remove the trailing whitespace so it doesn't end up // incorrectly concatenated with the indent of the following token. if token(at: lastNewlineIndex - 1)?.isSpace == true, token(at: lastNewlineIndex - 2)?.isLinebreak == true { removeToken(at: lastNewlineIndex - 1) } } } /// Ensures that given range starts with at least one leading blank line, /// by adding blank like to the start of this declaration if not already present. func addLeadingBlankLineIfNeeded(in range: ClosedRange) { let range = range.autoUpdating(in: self) while tokens[range].numberOfLeadingLinebreaks() < 2 { insertLinebreak(at: range.lowerBound) } } /// Ensures that the given range doesn't end with a trailing blank line /// by removing any trailing blank lines. func removeLeadingBlankLinesIfPresent(in range: ClosedRange) { let range = range.autoUpdating(in: self) while tokens[range].numberOfLeadingLinebreaks() > 1 { guard let firstNewlineIndex = index(of: .linebreak, in: Range(range.range)) else { break } removeTokens(in: range.lowerBound ... firstNewlineIndex) } } } extension RandomAccessCollection where Element == Token, Index == Int { /// The number of trailing newlines in this array of tokens, /// taking into account any spaces that may be between the linebreaks. func numberOfLeadingLinebreaks() -> Int { guard !isEmpty else { return 0 } var numberOfLeadingLinebreaks = 0 var searchIndex = indices.first! while searchIndex <= indices.last!, self[searchIndex].isSpaceOrLinebreak { if self[searchIndex].isLinebreak { numberOfLeadingLinebreaks += 1 } searchIndex += 1 } return numberOfLeadingLinebreaks } /// The number of trailing newlines in this array of tokens, /// taking into account any spaces that may be between the linebreaks. func numberOfTrailingLinebreaks() -> Int { guard !isEmpty else { return 0 } var numberOfTrailingLinebreaks = 0 var searchIndex = indices.last! while searchIndex >= indices.first!, self[searchIndex].isSpaceOrLinebreak { if self[searchIndex].isLinebreak { numberOfTrailingLinebreaks += 1 } searchIndex -= 1 } return numberOfTrailingLinebreaks } } extension Date { static var yearFormatter: (Date) -> String = { let formatter = DateFormatter() formatter.dateFormat = "yyyy" return { date in formatter.string(from: date) } }() static var currentYear = yearFormatter(Date()) var yearString: String { Date.yearFormatter(self) } func format(with format: DateFormat, timeZone: FormatTimeZone) -> String { let formatter = DateFormatter() if let chosenTimeZone = timeZone.timeZone { formatter.timeZone = chosenTimeZone } switch format { case .system: formatter.dateStyle = .short formatter.timeStyle = .none case .dayMonthYear: formatter.dateFormat = "dd/MM/yyyy" case .iso: formatter.dateFormat = "yyyy-MM-dd" case .monthDayYear: formatter.dateFormat = "MM/dd/yyyy" case let .custom(format): formatter.dateFormat = format } return formatter.string(from: self) } }