Files
SwiftFormat/Sources/FormattingHelpers.swift
T
2020-08-24 11:43:44 +01:00

407 lines
17 KiB
Swift

//
// 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 {
// remove self if possible
func removeSelf(at i: Int, localNames: Set<String>) -> Bool {
assert(tokens[i] == .identifier("self"))
guard let dotIndex = index(of: .nonSpaceOrLinebreak, after: i, if: {
$0 == .operator(".", .infix)
}), let nextIndex = index(of: .nonSpaceOrLinebreak, after: dotIndex, if: {
$0.isIdentifier && !localNames.contains($0.unescaped())
}), !backticksRequired(at: nextIndex, ignoreLeadingDot: true) else {
return false
}
removeTokens(in: i ..< nextIndex)
return true
}
// Shared wrap implementation
func wrapCollectionsAndArguments(completePartialWrapping: Bool, wrapSingleArguments: Bool) {
let maxWidth = options.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 wrapArgumentsBeforeFirst(startOfScope i: Int,
endOfScope: Int,
allowGrouping: Bool,
endOfScopeOnSameLine: Bool)
{
// Get indent
let indent = indentForLine(at: i)
var endOfScope = endOfScope
keepParameterLabelsOnSameLine(startOfScope: i,
endOfScope: &endOfScope)
if endOfScopeOnSameLine {
removeLinebreakBeforeEndOfScope(at: &endOfScope)
} else {
// 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
}
}
}
// Insert linebreak after each comma
var index = self.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, !options.truncateBlankLines ||
next(.nonSpace, after: linebreakIndex).map({ !$0.isLinebreak }) ?? false
{
insertSpace(indent + options.indent, at: linebreakIndex + 1)
} else if !allowGrouping || (maxWidth > 0 &&
lineLength(at: linebreakIndex) > maxWidth &&
lineLength(upTo: linebreakIndex) <= maxWidth)
{
insertLinebreak(at: linebreakIndex)
insertSpace(indent + options.indent, at: linebreakIndex + 1)
}
}
index = commaIndex
}
// Insert linebreak and indent after opening paren
if let nextIndex = self.index(of: .nonSpaceOrComment, after: i) {
if !tokens[nextIndex].isLinebreak {
insertLinebreak(at: nextIndex)
}
if nextIndex + 1 < endOfScope {
var indent = indent
if (self.index(of: .nonSpace, after: nextIndex) ?? 0) < endOfScope {
indent += options.indent
}
insertSpace(indent, at: nextIndex + 1)
}
}
}
func wrapArgumentsAfterFirst(startOfScope i: Int, endOfScope: Int, allowGrouping: Bool) {
guard var firstArgumentIndex = self.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)
}
}
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
var endOfScopeOnSameLine = false
let hasMultipleArguments = index(of: .delimiter(","), 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
}
endOfScopeOnSameLine = options.closingParenOnSameLine
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),
!isStringLiteral(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: firstIdentifierIndex > firstLinebreakIndex,
endOfScopeOnSameLine: endOfScopeOnSameLine)
case .preserve where firstIdentifierIndex > firstLinebreakIndex:
wrapArgumentsBeforeFirst(startOfScope: i,
endOfScope: endOfScope,
allowGrouping: true,
endOfScopeOnSameLine: endOfScopeOnSameLine)
case .afterFirst, .preserve:
wrapArgumentsAfterFirst(startOfScope: i,
endOfScope: endOfScope,
allowGrouping: true)
case .disabled, .default:
assertionFailure() // Shouldn't happen
}
} else if maxWidth > 0, hasMultipleArguments || wrapSingleArguments {
func willWrapAtStartOfReturnType(maxWidth: Int) -> Bool {
return 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,
endOfScopeOnSameLine: endOfScopeOnSameLine)
case .afterFirst:
wrapArgumentsAfterFirst(startOfScope: i,
endOfScope: endOfScope,
allowGrouping: true)
case .disabled, .default:
assertionFailure() // Shouldn't happen
}
}
if currentRule == FormatRules.wrap {
let nextWrapIndex = indexOfNextWrap() ?? endOfLine(at: i)
if nextWrapIndex > lastIndex,
maxWidth < lineLength(from: max(lastIndex, 0), upTo: nextWrapIndex),
!willWrapAtStartOfReturnType(maxWidth: maxWidth)
{
wrapArgumentsWithoutPartialWrapping()
lastIndex = nextWrapIndex
return
}
} else if maxWidth < lineLength(upTo: endOfScope) {
wrapArgumentsWithoutPartialWrapping()
}
}
lastIndex = i
}
}
func removeParen(at index: Int) {
func tokenOutsideParenRequiresSpacing(at index: Int) -> Bool {
guard let token = self.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 = self.token(at: index) else { return false }
switch token {
case .operator, .startOfScope("{"), .endOfScope("}"):
return true
default:
return tokenOutsideParenRequiresSpacing(at: index)
}
}
if token(at: index - 1)?.isSpace == true,
token(at: index + 1)?.isSpace == true
{
// Need to remove one
removeToken(at: index + 1)
} else if case .startOfScope = tokens[index] {
if tokenOutsideParenRequiresSpacing(at: index - 1),
tokenInsideParenRequiresSpacing(at: index + 1)
{
// Need to insert one
insert(.space(" "), at: index + 1)
}
} else if tokenInsideParenRequiresSpacing(at: index - 1),
tokenOutsideParenRequiresSpacing(at: index + 1)
{
// Need to insert one
insert(.space(" "), at: index + 1)
}
removeToken(at: index)
}
}