mirror of
https://github.com/nicklockwood/SwiftFormat.git
synced 2026-05-17 10:30:35 +00:00
a9ec7c34ce
Co-authored-by: calda <1811727+calda@users.noreply.github.com>
4194 lines
178 KiB
Swift
4194 lines
178 KiB
Swift
//
|
|
// ParsingHelpers.swift
|
|
// SwiftFormat
|
|
//
|
|
// Created by Nick Lockwood on 08/04/2019.
|
|
// Copyright © 2019 Nick Lockwood. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
// MARK: - shared helper methods
|
|
|
|
public extension Formatter {
|
|
/// Returns the index of the first token of the line containing the specified index
|
|
func startOfLine(at index: Int, excludingIndent: Bool = false) -> Int {
|
|
var index = min(index, tokens.count)
|
|
while let token = token(at: index - 1) {
|
|
if case .linebreak = token {
|
|
break
|
|
}
|
|
index -= 1
|
|
}
|
|
if excludingIndent, case .space? = token(at: index) {
|
|
return index + 1
|
|
}
|
|
return index
|
|
}
|
|
|
|
/// Returns the index of the linebreak token at the end of the line containing the specified index
|
|
func endOfLine(at index: Int) -> Int {
|
|
var index = index
|
|
while let token = token(at: index) {
|
|
if case .linebreak = token {
|
|
break
|
|
}
|
|
index += 1
|
|
}
|
|
return index
|
|
}
|
|
|
|
/// Whether or not the two indices represent tokens on the same line
|
|
func onSameLine(_ lhs: Int, _ rhs: Int) -> Bool {
|
|
startOfLine(at: lhs) == startOfLine(at: rhs)
|
|
}
|
|
|
|
/// Returns the current space at the start of the line containing the specified index
|
|
func currentIndentForLine(at index: Int) -> String {
|
|
if case let .space(string)? = token(at: startOfLine(at: index)) {
|
|
return string
|
|
}
|
|
return ""
|
|
}
|
|
|
|
/// Returns the length (in characters) of the specified token
|
|
func tokenLength(_ token: Token) -> Int {
|
|
let tabWidth = options.tabWidth > 0 ? options.tabWidth : options.indent.count
|
|
return token.columnWidth(tabWidth: tabWidth)
|
|
}
|
|
|
|
/// Returns the length (in characters) of the line at the specified index
|
|
func lineLength(at index: Int) -> Int {
|
|
lineLength(upTo: endOfLine(at: index))
|
|
}
|
|
|
|
/// Returns the length (in characters) up to (but not including) the specified token index
|
|
func lineLength(upTo index: Int) -> Int {
|
|
lineLength(from: startOfLine(at: index), upTo: index)
|
|
}
|
|
|
|
/// Returns the length (in characters) of the specified token range
|
|
func lineLength(from start: Int, upTo end: Int) -> Int {
|
|
if options.assetLiteralWidth == .actualWidth {
|
|
return tokens[start ..< end].reduce(0) { total, token in
|
|
total + tokenLength(token)
|
|
}
|
|
}
|
|
var length = 0
|
|
var index = start
|
|
while index < end {
|
|
let token = tokens[index]
|
|
switch token {
|
|
case .keyword("#colorLiteral"), .keyword("#imageLiteral"):
|
|
guard let startIndex = self.index(of: .startOfScope("("), after: index),
|
|
let endIndex = endOfScope(at: startIndex)
|
|
else {
|
|
fallthrough
|
|
}
|
|
length += 2 // visible length of asset literal in Xcode
|
|
index = endIndex + 1
|
|
default:
|
|
length += tokenLength(token)
|
|
index += 1
|
|
}
|
|
}
|
|
return length
|
|
}
|
|
|
|
/// Returns white space made up of indent characters equivalent to the specified width
|
|
func spaceEquivalentToWidth(_ width: Int) -> String {
|
|
if !options.smartTabs, options.useTabs, options.tabWidth > 0 {
|
|
let tabs = width / options.tabWidth
|
|
let remainder = width % options.tabWidth
|
|
return String(repeating: "\t", count: tabs) + String(repeating: " ", count: remainder)
|
|
}
|
|
return String(repeating: " ", count: width)
|
|
}
|
|
|
|
/// Returns white space made up of indent characters equvialent to the specified token range
|
|
func spaceEquivalentToTokens(from start: Int, upTo end: Int) -> String {
|
|
if !options.smartTabs, options.useTabs, options.tabWidth > 0 {
|
|
return spaceEquivalentToWidth(lineLength(from: start, upTo: end))
|
|
}
|
|
var result = ""
|
|
var index = start
|
|
while index < end {
|
|
let token = tokens[index]
|
|
switch token {
|
|
case let .space(string):
|
|
result += string
|
|
case .keyword("#colorLiteral"), .keyword("#imageLiteral"):
|
|
guard let startIndex = self.index(of: .startOfScope("("), after: index),
|
|
let endIndex = endOfScope(at: startIndex)
|
|
else {
|
|
fallthrough
|
|
}
|
|
let length = lineLength(from: index, upTo: endIndex + 1)
|
|
result += String(repeating: " ", count: length)
|
|
index = endIndex
|
|
default:
|
|
result += String(repeating: " ", count: tokenLength(token))
|
|
}
|
|
index += 1
|
|
}
|
|
return result
|
|
}
|
|
|
|
/// Returns the starting token for the containing scope at the specified index
|
|
func currentScope(at index: Int) -> Token? {
|
|
last(.startOfScope, before: index)
|
|
}
|
|
|
|
/// Returns the index of the starting token for the current scope
|
|
func startOfScope(at index: Int) -> Int? {
|
|
self.index(of: .startOfScope, before: index).flatMap {
|
|
if [.startOfScope("//"), .startOfScope("#!")].contains(tokens[$0]),
|
|
self.index(of: .linebreak, after: $0) ?? index < index
|
|
{
|
|
return nil
|
|
}
|
|
return $0
|
|
}
|
|
}
|
|
|
|
/// Returns the index of the ending token for the current scope
|
|
func endOfScope(at index: Int) -> Int? {
|
|
// TODO: should this return the closing `}` for `switch { ...` instead of nested `case`?
|
|
var startIndex: Int
|
|
guard var startToken = token(at: index) else { return nil }
|
|
if case .startOfScope = startToken {
|
|
startIndex = index
|
|
} else if let index = self.index(of: .startOfScope, before: index, if: {
|
|
![.startOfScope("//"), .startOfScope("#!")].contains($0)
|
|
}) {
|
|
startToken = tokens[index]
|
|
startIndex = index
|
|
} else {
|
|
return nil
|
|
}
|
|
guard startToken == .startOfScope("{") else {
|
|
var endIndex: Int? = startIndex
|
|
while let i = endIndex, i < index || !tokens[i].isEndOfScope(startToken) {
|
|
endIndex = self.index(after: i, where: {
|
|
$0.isEndOfScope(startToken) || $0 == .endOfScope("#endif")
|
|
})
|
|
}
|
|
assert(endIndex ?? index >= index)
|
|
return endIndex
|
|
}
|
|
while let endIndex = self.index(after: startIndex, where: {
|
|
$0.isEndOfScope(startToken)
|
|
}), let token = token(at: endIndex) {
|
|
if token == .endOfScope("}") {
|
|
assert(endIndex >= index)
|
|
return endIndex
|
|
}
|
|
startIndex = endIndex
|
|
startToken = token
|
|
}
|
|
return nil
|
|
}
|
|
|
|
/// Returns the end of the expression at the specified index, optionally stopping at any of the specified tokens
|
|
func endOfExpression(at index: Int, upTo delimiters: [Token]) -> Int? {
|
|
var index: Int? = index
|
|
if token(at: index!)?.isEndOfScope == true {
|
|
index = self.index(of: .nonSpaceOrLinebreak, after: index!)
|
|
}
|
|
var lastIndex = index
|
|
var wasOperator = true
|
|
while var i = index {
|
|
let token = tokens[i]
|
|
if delimiters.contains(token) {
|
|
return lastIndex
|
|
}
|
|
switch token {
|
|
case .operator(_, .infix):
|
|
wasOperator = true
|
|
case .operator(_, .prefix) where wasOperator, .operator(_, .postfix):
|
|
break
|
|
case .keyword("as"):
|
|
wasOperator = true
|
|
if self.token(at: i + 1)?.isUnwrapOperator == true {
|
|
i += 1
|
|
}
|
|
case .number, .identifier:
|
|
guard wasOperator else {
|
|
return lastIndex
|
|
}
|
|
wasOperator = false
|
|
case .startOfScope("<"),
|
|
.startOfScope where wasOperator,
|
|
.startOfScope("{") where isStartOfClosure(at: i),
|
|
.startOfScope("(") where isSubscriptOrFunctionCall(at: i),
|
|
.startOfScope("[") where isSubscriptOrFunctionCall(at: i):
|
|
wasOperator = false
|
|
guard let endIndex = endOfScope(at: i) else {
|
|
return nil
|
|
}
|
|
i = endIndex
|
|
default:
|
|
return lastIndex
|
|
}
|
|
lastIndex = i
|
|
index = self.index(of: .nonSpaceOrCommentOrLinebreak, after: i)
|
|
}
|
|
return lastIndex
|
|
}
|
|
}
|
|
|
|
enum ScopeType {
|
|
case array
|
|
case arrayType
|
|
case captureList
|
|
case dictionary
|
|
case dictionaryType
|
|
case functionCall
|
|
case parameterList
|
|
case `subscript`
|
|
case tuple
|
|
case tupleType
|
|
case throwsType
|
|
|
|
var isType: Bool {
|
|
switch self {
|
|
case .array, .captureList, .parameterList, .dictionary, .subscript, .functionCall, .tuple:
|
|
return false
|
|
case .arrayType, .dictionaryType, .tupleType:
|
|
return true
|
|
case .throwsType:
|
|
return false // the body is a type, but the scope/parens aren't part of it
|
|
}
|
|
}
|
|
}
|
|
|
|
extension Formatter {
|
|
/// Returns true if a token at this position is expected to be a type.
|
|
/// Note: Doesn't actually look at the token to see if it plausibly *is* a type.
|
|
func isTypePosition(at index: Int) -> Bool {
|
|
guard let prevIndex = self.index(
|
|
of: .nonSpaceOrCommentOrLinebreak,
|
|
before: index
|
|
) else {
|
|
return false
|
|
}
|
|
switch tokens[prevIndex] {
|
|
case .operator("->", .infix), .startOfScope("<"),
|
|
.keyword("is"), .keyword("as"):
|
|
return true
|
|
case .delimiter(":"), .delimiter(","):
|
|
// Check for property declaration
|
|
if let token = last(.keyword, before: index),
|
|
[.keyword("let"), .keyword("var")].contains(token)
|
|
{
|
|
return true
|
|
}
|
|
// Check for function declaration
|
|
if let scopeStart = startOfScope(at: index) {
|
|
switch tokens[scopeStart] {
|
|
case .startOfScope("("):
|
|
if last(.keyword, before: scopeStart) == .keyword("func") {
|
|
return true
|
|
}
|
|
fallthrough
|
|
case .startOfScope("["):
|
|
return isInClosureArguments(at: scopeStart)
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
fallthrough
|
|
default:
|
|
return scopeType(at: index)?.isType ?? false
|
|
}
|
|
}
|
|
|
|
/// Returns the type of the containing scope at the specified index
|
|
func scopeType(at index: Int) -> ScopeType? {
|
|
guard let token = token(at: index) else {
|
|
return nil
|
|
}
|
|
guard case .startOfScope = token else {
|
|
guard let startIndex = self.index(of: .startOfScope, before: index) else {
|
|
return nil
|
|
}
|
|
return scopeType(at: startIndex)
|
|
}
|
|
switch token {
|
|
case .startOfScope("("):
|
|
if last(.nonSpaceOrLinebreak, before: index) == .keyword("throws") {
|
|
return .throwsType
|
|
}
|
|
fallthrough
|
|
case .startOfScope("["):
|
|
guard let endIndex = endOfScope(at: index) else {
|
|
return nil
|
|
}
|
|
var isType = false
|
|
if let nextIndex = self.index(of: .nonSpaceOrCommentOrLinebreak, after: endIndex, if: {
|
|
$0.isOperator(".")
|
|
}), [.identifier("self"), .identifier("init")]
|
|
.contains(next(.nonSpaceOrCommentOrLinebreak, after: nextIndex))
|
|
{
|
|
isType = true
|
|
} else if next(.nonSpaceOrComment, after: endIndex) == .startOfScope("(") {
|
|
isType = true
|
|
} else if var prevIndex = self.index(of: .nonSpaceOrCommentOrLinebreak, before: index) {
|
|
if let attributeIndex = startOfAttribute(at: prevIndex) {
|
|
prevIndex = self.index(of: .nonSpaceOrCommentOrLinebreak, before: attributeIndex) ?? prevIndex
|
|
}
|
|
let prevToken = tokens[prevIndex]
|
|
switch prevToken {
|
|
case .operator where prevToken.isUnwrapOperator:
|
|
if ![.keyword("as"), .keyword("try")].contains(last(.nonSpaceOrComment, before: prevIndex)) {
|
|
fallthrough
|
|
}
|
|
isType = true
|
|
case .endOfScope where token.isStringDelimiter, .identifier, .endOfScope(")"), .endOfScope("]"):
|
|
if tokens[prevIndex + 1 ..< index].contains(where: \.isLinebreak) {
|
|
break
|
|
}
|
|
return token == .startOfScope("(") ? .functionCall : .subscript
|
|
case .startOfScope("{") where isInClosureArguments(at: index):
|
|
return token == .startOfScope("(") ? .parameterList : .captureList
|
|
case .delimiter(":"), .delimiter(","):
|
|
// Check for type declaration
|
|
if let scopeStart = self.index(of: .startOfScope, before: prevIndex) {
|
|
switch tokens[scopeStart] {
|
|
case .startOfScope("("):
|
|
if last(.keyword, before: scopeStart) == .keyword("func") {
|
|
isType = true
|
|
break
|
|
}
|
|
fallthrough
|
|
case .startOfScope("["):
|
|
guard let type = scopeType(at: scopeStart) else {
|
|
return nil
|
|
}
|
|
isType = type.isType
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
if let token = last(.keyword, before: index),
|
|
[.keyword("let"), .keyword("var")].contains(token)
|
|
{
|
|
isType = true
|
|
}
|
|
case .operator("->", _), .startOfScope("<"), .keyword("extension"):
|
|
isType = true
|
|
case .startOfScope("["), .startOfScope("("):
|
|
guard let type = scopeType(at: prevIndex) else {
|
|
return nil
|
|
}
|
|
isType = type.isType
|
|
case .operator("=", .infix):
|
|
isType = lastSignificantKeyword(at: prevIndex) == "typealias"
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
if token == .startOfScope("(") {
|
|
return isType ? .tupleType : .tuple
|
|
} else if self.index(of: .delimiter(":"), after: index) != nil {
|
|
return isType ? .dictionaryType : .dictionary
|
|
} else {
|
|
return isType ? .arrayType : .array
|
|
}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
/// Returns true if the token at specified index is a modifier
|
|
func isModifier(at index: Int) -> Bool {
|
|
guard let token = token(at: index), token.isModifierKeyword else {
|
|
return false
|
|
}
|
|
|
|
if token == .keyword("class"),
|
|
let nextToken = next(.nonSpaceOrCommentOrLinebreak, after: index)
|
|
{
|
|
return nextToken.isDeclarationTypeKeyword || nextToken.isModifierKeyword
|
|
}
|
|
|
|
// Async is only a valid modifier on local let/var declarations.
|
|
if token == .identifier("async") {
|
|
guard let nextDeclaration = self.index(after: index, where: \.isDeclarationTypeKeyword),
|
|
["let", "var"].contains(tokens[nextDeclaration].string)
|
|
else {
|
|
return false
|
|
}
|
|
|
|
// If we're inside a type body, this cannot be an `async let` declaration.
|
|
if let startOfScope = startOfScope(at: index),
|
|
let keyword = lastSignificantKeyword(at: startOfScope, excluding: ["where"]),
|
|
Token.swiftTypeKeywords.contains(keyword)
|
|
{
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
/// Returns true if the modifiers list for the given declaration contain a
|
|
/// modifier matching the specified predicate
|
|
func modifiersForDeclaration(at index: Int, contains: (Int, String) -> Bool) -> Bool {
|
|
var index = index
|
|
while var prevIndex = self.index(of: .nonSpaceOrCommentOrLinebreak, before: index) {
|
|
switch tokens[prevIndex] {
|
|
case let token where isModifier(at: prevIndex) || token.isAttribute:
|
|
if case .identifier = token,
|
|
let nextToken = last(.nonSpaceOrCommentOrLinebreak, before: prevIndex),
|
|
nextToken == .keyword("case") || nextToken.isOperator(ofType: .infix) || nextToken.isOperator(ofType: .prefix)
|
|
{
|
|
// Part of previous declaration
|
|
return false
|
|
}
|
|
|
|
// Modifiers can be fully-qualified types like `@ArrayBuilder<String>`, macros like `@Foo(.bar)`,
|
|
// or module-qualified attributes like `@SwiftUI::State`.
|
|
// Use the full modifier name instead of just the first token.
|
|
var modifierRange = prevIndex ... prevIndex
|
|
if token.isAttribute, let endOfAttr = endOfAttribute(at: prevIndex), endOfAttr > prevIndex {
|
|
modifierRange = prevIndex ... endOfAttr
|
|
} else if let nextIndex = self.index(of: .nonSpaceOrCommentOrLinebreak, after: prevIndex),
|
|
tokens[nextIndex] == .startOfScope("<") || tokens[nextIndex] == .startOfScope("("),
|
|
let endOfScope = endOfScope(at: nextIndex)
|
|
{
|
|
modifierRange = prevIndex ... endOfScope
|
|
}
|
|
|
|
if contains(prevIndex, tokens[modifierRange].string) {
|
|
return true
|
|
}
|
|
case .endOfScope(")"):
|
|
guard let startIndex = self.index(of: .startOfScope("("), before: prevIndex),
|
|
let identifierIndex = self.index(of: .nonSpaceOrCommentOrLinebreak, before: startIndex)
|
|
else {
|
|
return false
|
|
}
|
|
let identifierToken = tokens[identifierIndex]
|
|
guard identifierToken.isAttribute
|
|
|| _FormatRules.allModifiers.contains(identifierToken.string)
|
|
|| identifierToken == .endOfScope(">")
|
|
|| (identifierToken.isIdentifier
|
|
&& self.index(of: .nonSpaceOrCommentOrLinebreak, before: identifierIndex, if: { $0.isOperator("::") }) != nil)
|
|
else {
|
|
return false
|
|
}
|
|
if let prevToken = last(.nonSpaceOrCommentOrLinebreak, before: identifierIndex),
|
|
prevToken == .delimiter(",") || prevToken.isDeclarationTypeKeyword
|
|
{
|
|
return false
|
|
}
|
|
prevIndex = startIndex
|
|
case .endOfScope(">"):
|
|
guard let startIndex = self.index(of: .startOfScope("<"), before: prevIndex),
|
|
last(.nonSpaceOrCommentOrLinebreak, before: startIndex, if: { $0.isAttribute }) != nil
|
|
else {
|
|
return false
|
|
}
|
|
prevIndex = startIndex
|
|
case .identifier:
|
|
guard let startIndex = startOfAttribute(at: prevIndex),
|
|
let nextIndex = self.index(of: .nonSpaceOrCommentOrLinebreak, after: startIndex, if: {
|
|
$0.isOperator(".") || $0.isOperator("::")
|
|
})
|
|
else {
|
|
return false
|
|
}
|
|
prevIndex = nextIndex
|
|
default:
|
|
return false
|
|
}
|
|
index = prevIndex
|
|
}
|
|
return false
|
|
}
|
|
|
|
/// Returns true if the modifiers list for the given declaration contain the
|
|
/// specified modifier, ignoring any arguments that the modifier may have.
|
|
func modifiersForDeclaration(at index: Int, contains expectedModifier: String) -> Bool {
|
|
modifiersForDeclaration(at: index, contains: { firstIndex, fullModifier in
|
|
tokens[firstIndex].string == expectedModifier || fullModifier == expectedModifier
|
|
})
|
|
}
|
|
|
|
/// Returns the index of the specified modifier for a given declaration, or
|
|
/// nil if the type doesn't have that modifier
|
|
func indexOfModifier(_ modifier: String, forDeclarationAt index: Int) -> Int? {
|
|
var i: Int?
|
|
return modifiersForDeclaration(at: index, contains: {
|
|
i = $0
|
|
return $1 == modifier
|
|
}) ? i : nil
|
|
}
|
|
|
|
/// Returns the index of the first modifier in a list
|
|
func startOfModifiers(at index: Int, includingAttributes: Bool) -> Int {
|
|
var startIndex = index
|
|
_ = modifiersForDeclaration(at: index, contains: { i, name in
|
|
if !includingAttributes, name.isAttribute {
|
|
return true
|
|
}
|
|
startIndex = i
|
|
return false
|
|
})
|
|
return startIndex
|
|
}
|
|
|
|
/// Return true if token at specified index in a function in the given list
|
|
func isSymbol(at i: Int, in names: Set<String>) -> Bool {
|
|
// TODO: more sophisticated checks involving full signature, namespace, etc
|
|
guard let name = token(at: i)?.unescaped() else {
|
|
return false
|
|
}
|
|
return names.contains(name)
|
|
}
|
|
|
|
/// Gather declared variable names, starting at index after let/var keyword
|
|
func processDeclaredVariables(at index: inout Int, names: inout Set<String>) {
|
|
processDeclaredVariables(at: &index, names: &names, removeSelfKeyword: nil,
|
|
onlyLocal: false, scopeAllowsImplicitSelfRebinding: false)
|
|
}
|
|
|
|
/// Returns true if token is inside the return type of a function or subscript
|
|
func isInReturnType(at i: Int) -> Bool {
|
|
startOfReturnType(at: i) != nil
|
|
}
|
|
|
|
/// Returns the index of the `->` operator for the current return type declaration if
|
|
/// the specified index is in a return type declaration.
|
|
func startOfReturnType(at i: Int) -> Int? {
|
|
guard let startIndex = indexOfLastSignificantKeyword(
|
|
at: i, excluding: ["throws", "rethrows"]
|
|
), ["func", "subscript"].contains(tokens[startIndex].string) else {
|
|
return nil
|
|
}
|
|
|
|
let endIndex = index(of: .startOfScope("{"), after: i) ?? i
|
|
|
|
return index(of: .operator("->", .infix), in: startIndex + 1 ..< endIndex)
|
|
}
|
|
|
|
/// Recursively searches to the start of scopes until we either no longer find a scope or we know we are in a closure.
|
|
func isInClosure(at index: Int) -> Bool {
|
|
guard let startOfScopeIndex = startOfScope(at: index) else {
|
|
return false
|
|
}
|
|
if isStartOfClosure(at: startOfScopeIndex) {
|
|
return true
|
|
} else {
|
|
return isInClosure(at: startOfScopeIndex)
|
|
}
|
|
}
|
|
|
|
/// Whether the given index is within the body of the given function declaration,
|
|
/// and not inside a nested closure or nested function.
|
|
func isInFunctionBody(of functionDecl: FunctionDeclaration, at index: Int) -> Bool {
|
|
guard let bodyRange = functionDecl.bodyRange,
|
|
bodyRange.contains(index)
|
|
else {
|
|
return false
|
|
}
|
|
|
|
guard let startOfScopeIndex = startOfScope(at: index) else {
|
|
return false
|
|
}
|
|
|
|
if startOfScopeIndex == bodyRange.lowerBound {
|
|
return true
|
|
}
|
|
|
|
if isStartOfClosure(at: startOfScopeIndex) {
|
|
return false
|
|
}
|
|
|
|
// If this is a function scope, but not the body of the function itself,
|
|
// then this is some nested function.
|
|
if lastSignificantKeyword(at: startOfScopeIndex, excluding: ["where"]) == "func",
|
|
startOfScopeIndex != bodyRange.lowerBound
|
|
{
|
|
return false
|
|
}
|
|
|
|
// Recursively check parent scope
|
|
return isInFunctionBody(of: functionDecl, at: startOfScopeIndex)
|
|
}
|
|
|
|
/// Whether or not the given index is within a string body or string interpolation
|
|
func isInStringInterpolation(at index: Int) -> Bool {
|
|
guard let startOfScopeIndex = startOfScope(at: index) else {
|
|
return false
|
|
}
|
|
|
|
// If the code is in a string, then it could be inside a string interpolation
|
|
if tokens[startOfScopeIndex] == .startOfScope("\"") || tokens[startOfScopeIndex] == .startOfScope("\"\"\"") {
|
|
return true
|
|
}
|
|
|
|
return isInStringInterpolation(at: startOfScopeIndex)
|
|
}
|
|
|
|
/// Whether or not the `try` keyword is supported at the given index
|
|
/// within the given function declaration, if it were throwing.
|
|
func tryKeywordSupported(at index: Int, in functionDecl: FunctionDeclaration) -> Bool {
|
|
isInFunctionBody(of: functionDecl, at: index)
|
|
// String interpolation is a non-throwing autoclosure, so can't use `try`
|
|
&& !isInStringInterpolation(at: index)
|
|
}
|
|
|
|
/// Whether or not this index the start of scope of a closure literal, eg `{` but not some other type of scope.
|
|
func isStartOfClosure(at i: Int) -> Bool {
|
|
guard token(at: i) == .startOfScope("{") else {
|
|
return false
|
|
}
|
|
if isConditionalStatement(at: i) {
|
|
if let endIndex = endOfScope(at: i),
|
|
[.startOfScope("("), .operator(".", .infix)]
|
|
.contains(next(.nonSpaceOrComment, after: endIndex) ?? .space("")) ||
|
|
next(.nonSpaceOrCommentOrLinebreak, after: endIndex) == .startOfScope("{")
|
|
{
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
guard var prevIndex = index(of: .nonSpaceOrCommentOrLinebreak, before: i) else {
|
|
return true
|
|
}
|
|
switch tokens[prevIndex] {
|
|
case .startOfScope("("), .startOfScope("["), .startOfScope("{"),
|
|
.operator(_, .infix), .operator(_, .prefix), .delimiter, .keyword("return"),
|
|
.keyword("in"), .keyword("where"), .keyword("try"), .keyword("throw"), .keyword("await"):
|
|
return true
|
|
case .operator(_, .none),
|
|
.keyword("deinit"), .keyword("catch"), .keyword("else"), .keyword("repeat"),
|
|
.keyword("throws"), .keyword("rethrows"):
|
|
return false
|
|
case .endOfScope("}"):
|
|
guard let startOfScope = index(of: .startOfScope("{"), before: prevIndex) else {
|
|
return false
|
|
}
|
|
return !isStartOfClosure(at: startOfScope)
|
|
case .endOfScope(")"), .endOfScope(">"):
|
|
guard var startOfScope = index(of: .startOfScope, before: prevIndex),
|
|
var prev = index(of: .nonSpaceOrCommentOrLinebreak, before: startOfScope)
|
|
else {
|
|
return true
|
|
}
|
|
if tokens[prevIndex] == .endOfScope(">"), tokens[prev] == .endOfScope(")") {
|
|
startOfScope = index(of: .startOfScope, before: prev) ?? startOfScope
|
|
prev = index(of: .nonSpaceOrCommentOrLinebreak, before: startOfScope) ?? prev
|
|
}
|
|
switch tokens[prev] {
|
|
case .identifier:
|
|
prevIndex = prev
|
|
case .operator("?", .postfix), .operator("!", .postfix):
|
|
switch token(at: prev - 1) {
|
|
case .identifier?:
|
|
prevIndex = prev - 1
|
|
case .keyword("init")?:
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
case .operator("->", .infix), .keyword("init"), .keyword("subscript"), .keyword("throws"):
|
|
return false
|
|
case .endOfScope(">"):
|
|
guard let startIndex = index(of: .startOfScope("<"), before: prev) else {
|
|
fatalError("Expected <", at: prev - 1)
|
|
return false
|
|
}
|
|
guard let prevIndex = index(of: .nonSpaceOrComment, before: startIndex, if: {
|
|
$0.isIdentifier
|
|
}) else {
|
|
return false
|
|
}
|
|
return last(.nonSpaceOrCommentOrLinebreak, before: prevIndex) != .keyword("func")
|
|
default:
|
|
return false
|
|
}
|
|
fallthrough
|
|
case .identifier, .number, .operator("?", .postfix), .operator("!", .postfix),
|
|
.endOfScope where tokens[prevIndex].isStringDelimiter:
|
|
if let nextIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: i),
|
|
isAccessorKeyword(at: nextIndex) || isAccessorKeyword(at: prevIndex)
|
|
{
|
|
return false
|
|
}
|
|
guard let prevKeywordIndex = indexOfLastSignificantKeyword(at: prevIndex, excluding: ["where"]) else {
|
|
return true
|
|
}
|
|
switch tokens[prevKeywordIndex].string {
|
|
case "var":
|
|
if lastIndex(of: .operator("=", .infix), in: prevKeywordIndex + 1 ..< i) != nil {
|
|
return true
|
|
}
|
|
var index = prevKeywordIndex
|
|
while let nextIndex = self.index(of: .nonSpaceOrComment, after: index),
|
|
nextIndex < i
|
|
{
|
|
switch tokens[nextIndex] {
|
|
case .operator("=", .infix):
|
|
return true
|
|
case .linebreak:
|
|
guard let nextIndex =
|
|
self.index(of: .nonSpaceOrCommentOrLinebreak, after: nextIndex)
|
|
else {
|
|
return true
|
|
}
|
|
if tokens[nextIndex] != .startOfScope("{"),
|
|
isEndOfStatement(at: index), isStartOfStatement(at: nextIndex)
|
|
{
|
|
return true
|
|
}
|
|
index = nextIndex
|
|
default:
|
|
index = nextIndex
|
|
}
|
|
}
|
|
return false
|
|
case "class", "actor", "struct", "enum", "protocol", "extension",
|
|
"func", "init", "subscript", "catch":
|
|
return false
|
|
case "throws", "rethrows":
|
|
return next(.keyword, after: prevKeywordIndex) == .keyword("in")
|
|
default:
|
|
return true
|
|
}
|
|
case .keyword, .endOfScope("]"), .endOfScope(">"):
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
}
|
|
|
|
/// Whether or not the index is the start of a valid closure type
|
|
func isStartOfClosureType(at i: Int) -> Bool {
|
|
guard let type = parseType(at: i),
|
|
index(of: .operator("->", .infix), in: type.range) != nil
|
|
else { return false }
|
|
|
|
// Avoid confusing the arguments + return type of a function declaration with a closure type
|
|
if let previousKeyword = indexOfLastSignificantKeyword(at: i, excluding: ["where"]),
|
|
tokens[previousKeyword] == .keyword("func"),
|
|
let functionDeclaration = parseFunctionDeclaration(keywordIndex: previousKeyword),
|
|
functionDeclaration.argumentsRange.lowerBound == type.range.lowerBound
|
|
{
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
/// Returns true if the index is within a closure's argument list (between `{` and `in`).
|
|
func isInClosureArguments(at i: Int) -> Bool {
|
|
// Find the enclosing `{` scope, walking past any nested scopes
|
|
var scopeStart = i
|
|
while let startIndex = startOfScope(at: scopeStart) {
|
|
if tokens[startIndex] == .startOfScope("{") {
|
|
guard isStartOfClosure(at: startIndex),
|
|
let closureArgs = parseClosureArguments(at: startIndex)
|
|
else {
|
|
return false
|
|
}
|
|
return i > startIndex && i <= closureArgs.inKeywordIndex
|
|
}
|
|
scopeStart = startIndex
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isAccessorKeyword(at i: Int, checkKeyword: Bool = true) -> Bool {
|
|
guard !checkKeyword ||
|
|
["get", "set", "willSet", "didSet", "init", "_modify"].contains(token(at: i)?.string ?? ""),
|
|
var prevIndex = index(of: .nonSpaceOrCommentOrLinebreak, before: i)
|
|
else {
|
|
return false
|
|
}
|
|
if tokens[prevIndex] == .endOfScope("}"),
|
|
let startIndex = index(of: .startOfScope("{"), before: prevIndex),
|
|
let prev = index(of: .nonSpaceOrCommentOrLinebreak, before: startIndex)
|
|
{
|
|
prevIndex = prev
|
|
if tokens[prevIndex] == .endOfScope(")"),
|
|
let startIndex = index(of: .startOfScope("("), before: prevIndex),
|
|
let prev = index(of: .nonSpaceOrCommentOrLinebreak, before: startIndex)
|
|
{
|
|
prevIndex = prev
|
|
}
|
|
return isAccessorKeyword(at: prevIndex)
|
|
} else if tokens[prevIndex] == .startOfScope("{") {
|
|
switch lastSignificantKeyword(at: prevIndex, excluding: ["where"]) {
|
|
case "var"?, "subscript"?:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
/// Returns true if the token at the specified index is part of a conditional statement
|
|
func isConditionalStatement(at i: Int, excluding: Set<String> = []) -> Bool {
|
|
startOfConditionalStatement(at: i, excluding: excluding) != nil
|
|
}
|
|
|
|
/// Returns true if the token at the specified index is part of a conditional assignment
|
|
/// (e.g. an if or switch expression following an `=` token)
|
|
func isConditionalAssignment(at i: Int) -> Bool {
|
|
guard let startOfConditional = startOfConditionalStatement(at: i),
|
|
let previousToken = lastToken(before: startOfConditional, where: { !$0.isSpaceOrCommentOrLinebreak })
|
|
else { return false }
|
|
|
|
return previousToken.isOperator("=")
|
|
}
|
|
|
|
/// If the token at the specified index is part of a conditional statement, returns the index of the first
|
|
/// token in the statement (e.g. `if`, `guard`, `while`, etc.), otherwise returns nil
|
|
func startOfConditionalStatement(at i: Int, excluding: Set<String> = []) -> Int? {
|
|
guard var index = indexOfLastSignificantKeyword(at: i, excluding: excluding.union(["else"])) else {
|
|
return nil
|
|
}
|
|
|
|
if tokens[index] == .keyword("where") {
|
|
if lastToken(before: index, where: {
|
|
[.endOfScope("case"), .endOfScope("}")].contains($0)
|
|
}) == .endOfScope("case") {
|
|
return nil
|
|
}
|
|
index = indexOfLastSignificantKeyword(at: index, excluding: ["where"]) ?? index
|
|
}
|
|
|
|
if tokens[index] == .keyword("case"), let i = self.index(
|
|
of: .nonSpaceOrCommentOrLinebreak,
|
|
before: index,
|
|
if: { $0 != .delimiter(",") }
|
|
) {
|
|
index = i
|
|
}
|
|
|
|
switch tokens[index].string {
|
|
case "let", "var", "await":
|
|
guard let prevIndex = self
|
|
.index(of: .nonSpaceOrCommentOrLinebreak, before: index)
|
|
else {
|
|
return nil
|
|
}
|
|
switch tokens[prevIndex] {
|
|
case let .keyword(name) where
|
|
["if", "guard", "while", "for", "case", "catch"].contains(name):
|
|
return prevIndex
|
|
case .delimiter(","):
|
|
return startOfConditionalStatement(at: prevIndex)
|
|
default:
|
|
return nil
|
|
}
|
|
case "if", "guard", "while", "for", "repeat", "case":
|
|
return index
|
|
case "switch":
|
|
return next(.startOfScope, after: i) == .startOfScope(":") ? nil : index
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func lastSignificantKeyword(at i: Int, excluding: Set<String> = []) -> String? {
|
|
guard let index = indexOfLastSignificantKeyword(at: i, excluding: excluding),
|
|
case let .keyword(keyword) = tokens[index]
|
|
else {
|
|
return nil
|
|
}
|
|
return keyword
|
|
}
|
|
|
|
/// Returns true if the given index is inside a protocol declaration
|
|
func isInsideProtocol(at index: Int) -> Bool {
|
|
guard let scopeStart = startOfScope(at: index) else {
|
|
return false
|
|
}
|
|
|
|
// Exclude "class" because `protocol Foo: class { }` uses class as a constraint, not a type keyword
|
|
return lastSignificantKeyword(at: scopeStart, excluding: ["where", "class"]) == "protocol"
|
|
}
|
|
|
|
func indexOfLastSignificantKeyword(at i: Int, excluding: Set<String> = []) -> Int? {
|
|
guard let token = token(at: i),
|
|
let index = token.isKeyword ? i : index(of: .keyword, before: i),
|
|
case let .keyword(keyword) = tokens[index]
|
|
else {
|
|
return nil
|
|
}
|
|
|
|
func isAfterBrace(_ index: Int, _ i: Int) -> Bool {
|
|
if let scopeStart = lastIndex(of: .startOfScope, in: index ..< i) {
|
|
return isAfterBrace(index, scopeStart)
|
|
}
|
|
guard let braceIndex = lastIndex(
|
|
of: .endOfScope("}"),
|
|
in: index ..< i
|
|
) else {
|
|
return false
|
|
}
|
|
guard let nextToken = next(.nonSpaceOrComment, after: braceIndex),
|
|
!nextToken.isOperator(ofType: .infix),
|
|
!nextToken.isOperator(ofType: .postfix),
|
|
nextToken != .startOfScope("("),
|
|
nextToken != .startOfScope("{"),
|
|
nextToken != .delimiter(",")
|
|
else {
|
|
return isAfterBrace(index, braceIndex)
|
|
}
|
|
return true
|
|
}
|
|
|
|
if isAfterBrace(index, i) {
|
|
return nil
|
|
}
|
|
|
|
switch keyword {
|
|
case let name where name.hasPrefix("#") || excluding.contains(name):
|
|
fallthrough
|
|
case "in", "is", "as", "try", "await":
|
|
return indexOfLastSignificantKeyword(at: index - 1, excluding: excluding)
|
|
default:
|
|
return index
|
|
}
|
|
}
|
|
|
|
/// Returns true if the token at the specified index is part of an @attribute
|
|
func isAttribute(at i: Int) -> Bool {
|
|
startOfAttribute(at: i) != nil
|
|
}
|
|
|
|
/// If the token at the specified index is part of an @attribute, returns the index of the first
|
|
/// token in the attribute
|
|
func startOfAttribute(at i: Int) -> Int? {
|
|
switch tokens[i] {
|
|
case let token where token.isAttribute:
|
|
return i
|
|
case .endOfScope(")"):
|
|
guard let openParenIndex = index(of: .startOfScope("("), before: i),
|
|
let prevTokenIndex = index(of: .nonSpaceOrComment, before: openParenIndex)
|
|
else {
|
|
return nil
|
|
}
|
|
return startOfAttribute(at: prevTokenIndex)
|
|
case .identifier:
|
|
guard let separatorIndex = index(of: .nonSpaceOrCommentOrLinebreak, before: i, if: {
|
|
$0.isOperator(".") || $0.isOperator("::")
|
|
}), let prevTokenIndex = index(of: .nonSpaceOrCommentOrLinebreak, before: separatorIndex) else {
|
|
return nil
|
|
}
|
|
return startOfAttribute(at: prevTokenIndex)
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
/// If the token at the specified index is part of an @attribute, returns the index of the last
|
|
/// token in the attribute
|
|
func endOfAttribute(at i: Int) -> Int? {
|
|
guard let startIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: i) else {
|
|
return i
|
|
}
|
|
switch tokens[startIndex] {
|
|
case .startOfScope("(") where !tokens[i + 1 ..< startIndex].contains(where: \.isLinebreak):
|
|
guard let closeParenIndex = index(of: .endOfScope(")"), after: startIndex) else {
|
|
return nil
|
|
}
|
|
return closeParenIndex
|
|
case .operator(".", .infix):
|
|
guard let nextIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: startIndex) else {
|
|
return nil
|
|
}
|
|
return endOfAttribute(at: nextIndex)
|
|
case .operator("::", .infix) where !tokens[i + 1 ..< startIndex].contains(where: \.isLinebreak):
|
|
guard let nextIndex = index(of: .nonSpaceOrComment, after: startIndex),
|
|
!tokens[nextIndex].isLinebreak
|
|
else {
|
|
return i
|
|
}
|
|
return endOfAttribute(at: nextIndex)
|
|
case .startOfScope("<"):
|
|
guard let nextIndex = index(of: .endOfScope(">"), after: startIndex) else {
|
|
return nil
|
|
}
|
|
return endOfAttribute(at: nextIndex)
|
|
default:
|
|
return i
|
|
}
|
|
}
|
|
|
|
/// Whether or not this property at the given introducer index (either `var` or `let`)
|
|
/// is a stored property or a computed property.
|
|
func isStoredProperty(atIntroducerIndex introducerIndex: Int) -> Bool {
|
|
guard let property = parsePropertyDeclaration(atIntroducerIndex: introducerIndex) else {
|
|
return false
|
|
}
|
|
|
|
// If this property doesn't have a body, then it's definitely a stored property.
|
|
guard let bodyRange = property.body?.range,
|
|
let firstTokenInBody = token(at: bodyRange.lowerBound)
|
|
else {
|
|
return true
|
|
}
|
|
|
|
// If this property has a body, then its a stored property if and only if the body
|
|
// has a `didSet` or `willSet` keyword, based on the grammar for a variable declaration.
|
|
return [.identifier("willSet"), .identifier("didSet")].contains(firstTokenInBody)
|
|
}
|
|
|
|
/// Determine if next line after this token should be indented
|
|
func isEndOfStatement(at i: Int, in scope: Token? = nil) -> Bool {
|
|
guard let token = token(at: i) else { return true }
|
|
switch token {
|
|
case .endOfScope("case"), .endOfScope("default"):
|
|
return false
|
|
case let .keyword(string):
|
|
// TODO: handle context-specific keywords
|
|
// associativity, convenience, dynamic, didSet, final, get, infix, indirect,
|
|
// lazy, left, mutating, none, nonmutating, open, optional, override, postfix,
|
|
// precedence, prefix, Protocol, required, right, set, Type, unowned, weak, willSet
|
|
switch string {
|
|
case "let", "func", "var", "if", "as", "import", "try", "guard", "case",
|
|
"for", "init", "switch", "throw", "where", "subscript", "is",
|
|
"while", "associatedtype", "inout", "await":
|
|
return false
|
|
case "in":
|
|
return lastSignificantKeyword(at: i) != "for"
|
|
case "return":
|
|
guard let nextToken = next(.nonSpaceOrCommentOrLinebreak, after: i) else {
|
|
return true
|
|
}
|
|
switch nextToken {
|
|
case .keyword where !nextToken.isMacro, .endOfScope("case"), .endOfScope("default"):
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
default:
|
|
return true
|
|
}
|
|
case .delimiter(","):
|
|
// For arrays or argument lists, we already indent
|
|
guard let scope = scope ?? currentScope(at: i) else { return false }
|
|
return ["<", "[", "(", "case", "default"].contains(scope.string)
|
|
case .delimiter(":"):
|
|
guard let scope = scope ?? currentScope(at: i) else {
|
|
return false
|
|
}
|
|
// For arrays or argument lists, we already indent
|
|
return ["case", "default", "("].contains(scope.string)
|
|
case .operator(_, .infix), .operator(_, .prefix):
|
|
return false
|
|
case .operator("?", .postfix), .operator("!", .postfix):
|
|
switch self.token(at: i - 1) {
|
|
case .keyword("as")?, .keyword("try")?:
|
|
return false
|
|
default:
|
|
return true
|
|
}
|
|
default:
|
|
if let attributeIndex = startOfAttribute(at: i),
|
|
let prevIndex = index(of: .nonSpaceOrCommentOrLinebreak, before: attributeIndex)
|
|
{
|
|
return isEndOfStatement(at: prevIndex, in: scope)
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
/// Determine if line starting with this token should be indented
|
|
func isStartOfStatement(at i: Int, in scope: Token? = nil,
|
|
treatingCollectionKeysAsStart: Bool = true) -> Bool
|
|
{
|
|
guard let token = token(at: i) else { return true }
|
|
switch token {
|
|
case let .keyword(string) where [
|
|
"where", "dynamicType", "rethrows", "throws",
|
|
].contains(string):
|
|
return false
|
|
case .keyword("as"):
|
|
// For case statements, we already indent
|
|
return (scope ?? currentScope(at: i))?.string == "case"
|
|
case .keyword("in"):
|
|
let scope = (scope ?? currentScope(at: i))?.string
|
|
// For case statements and closures, we already indent
|
|
return scope == "case" || (scope == "{" && lastSignificantKeyword(at: i) != "for")
|
|
case .keyword("is"):
|
|
guard let lastToken = last(.nonSpaceOrCommentOrLinebreak, before: i) else {
|
|
return false
|
|
}
|
|
return [.endOfScope("case"), .keyword("case"), .delimiter(",")].contains(lastToken)
|
|
case .space, .delimiter, .operator(_, .infix), .operator(_, .postfix),
|
|
.endOfScope("}"), .endOfScope("]"), .endOfScope(")"), .endOfScope(">"),
|
|
.identifier where isTrailingClosureLabel(at: i):
|
|
return false
|
|
case .startOfScope("{") where isStartOfClosure(at: i):
|
|
guard last(.nonSpaceOrComment, before: i)?.isLinebreak == true,
|
|
let prevIndex = index(of: .nonSpaceOrCommentOrLinebreak, before: i),
|
|
let prevToken = self.token(at: prevIndex)
|
|
else {
|
|
return false
|
|
}
|
|
if prevToken.isIdentifier, !["true", "false", "nil"].contains(prevToken.string) {
|
|
return false
|
|
}
|
|
if [.endOfScope(")"), .endOfScope("]")].contains(prevToken),
|
|
let startIndex = index(of: .startOfScope, before: prevIndex),
|
|
!tokens[startIndex ..< prevIndex].contains(where: \.isLinebreak)
|
|
|| currentIndentForLine(at: startIndex) == currentIndentForLine(at: prevIndex)
|
|
{
|
|
return false
|
|
}
|
|
return true
|
|
case .identifier("async"):
|
|
if next(.nonSpaceOrCommentOrLinebreak, after: i) == .keyword("let") {
|
|
return true
|
|
}
|
|
if last(.nonSpaceOrCommentOrLinebreak, before: i) == .endOfScope(")"),
|
|
lastSignificantKeyword(at: i) == "func"
|
|
{
|
|
return false
|
|
}
|
|
fallthrough
|
|
case .startOfScope where token.isStringDelimiter && !treatingCollectionKeysAsStart,
|
|
.number where !treatingCollectionKeysAsStart, .keyword where token.isMacro, .identifier:
|
|
if !treatingCollectionKeysAsStart,
|
|
let prevIndex = index(of: .nonSpaceOrCommentOrLinebreak, before: i),
|
|
case let prevToken = tokens[prevIndex], [
|
|
.delimiter(","), .startOfScope("["), .startOfScope("(")
|
|
].contains(prevToken) || (
|
|
[
|
|
.operator("?", .postfix), .operator("!", .postfix),
|
|
].contains(prevToken) && [
|
|
.keyword("try"), .keyword("as"),
|
|
].contains(last(.nonSpaceOrComment, before: prevIndex) ?? .space(""))
|
|
)
|
|
{
|
|
return false
|
|
}
|
|
fallthrough
|
|
case .keyword("try"), .keyword("await"):
|
|
guard let prevIndex = index(of: .nonSpaceOrCommentOrLinebreak, before: i) else {
|
|
return true
|
|
}
|
|
let prevToken = tokens[prevIndex]
|
|
switch prevToken {
|
|
case .operator("?", .postfix)
|
|
where [.keyword("try"), .keyword("as")].contains(
|
|
last(.nonSpaceOrCommentOrLinebreak, before: prevIndex) ?? .space("")
|
|
),
|
|
.operator("!", .postfix)
|
|
where [.keyword("try"), .keyword("as")].contains(
|
|
last(.nonSpaceOrCommentOrLinebreak, before: prevIndex) ?? .space("")
|
|
):
|
|
return false
|
|
case .number, .operator(_, .postfix), .endOfScope, .identifier,
|
|
.startOfScope("{"), .startOfScope(":"), .delimiter(";"),
|
|
.keyword("in") where lastSignificantKeyword(at: i) != "for",
|
|
.keyword("#else"):
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
case .keyword:
|
|
return true
|
|
default:
|
|
guard let prevToken = last(.nonSpaceOrComment, before: i) else {
|
|
return true
|
|
}
|
|
guard prevToken.isLinebreak else {
|
|
return false
|
|
}
|
|
if let prevToken = last(.nonSpaceOrCommentOrLinebreak, before: i),
|
|
prevToken == .keyword("return") || prevToken.isOperator(ofType: .infix) ||
|
|
currentScope(at: i)?.isMultilineStringDelimiter == true
|
|
{
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
}
|
|
|
|
/// Whether the given index is a `startOfScope("{")` that represents the start of a type body
|
|
func isStartOfTypeBody(at scopeIndex: Int) -> Bool {
|
|
guard tokens[scopeIndex] == .startOfScope("{") else { return false }
|
|
|
|
guard let lastKeyword = lastSignificantKeyword(at: scopeIndex, excluding: ["where"]) else {
|
|
return false
|
|
}
|
|
|
|
return Token.swiftTypeKeywords.contains(lastKeyword)
|
|
}
|
|
|
|
func isTrailingClosureLabel(at i: Int) -> Bool {
|
|
if case .identifier? = token(at: i),
|
|
last(.nonSpaceOrCommentOrLinebreak, before: i) == .endOfScope("}"),
|
|
let nextIndex = index(of: .nonSpaceOrComment, after: i, if: { $0 == .delimiter(":") }),
|
|
let nextNextIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: nextIndex),
|
|
isStartOfClosure(at: nextNextIndex)
|
|
{
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isSubscriptOrFunctionCall(at i: Int) -> Bool {
|
|
guard case let .startOfScope(string)? = token(at: i), ["[", "("].contains(string),
|
|
let prevToken = last(.nonSpaceOrComment, before: i)
|
|
else {
|
|
return false
|
|
}
|
|
switch prevToken {
|
|
case .identifier, .operator(_, .postfix),
|
|
.endOfScope("]"), .endOfScope(")"), .endOfScope(">"), .endOfScope("}"),
|
|
.endOfScope where prevToken.isStringDelimiter:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
/// Crude check to detect if code is inside a Result Builder
|
|
/// Note: this will produce false positives for any init that takes a closure
|
|
func isInResultBuilder(at i: Int) -> Bool {
|
|
var i = i
|
|
while let startIndex = index(before: i, where: {
|
|
[.startOfScope("{"), .startOfScope(":"), .startOfScope("#if")].contains($0)
|
|
}) {
|
|
guard let prevIndex = index(before: startIndex, where: {
|
|
!$0.isSpaceOrCommentOrLinebreak && !$0.isEndOfScope
|
|
}) else {
|
|
return false
|
|
}
|
|
|
|
// Check if this is a type definition rather than a result builder
|
|
if tokens[startIndex] == .startOfScope("{"),
|
|
let lastKeyword = lastSignificantKeyword(at: startIndex, excluding: ["where"]),
|
|
Token.swiftTypeKeywords.contains(lastKeyword)
|
|
{
|
|
// This is a type body, not a result builder
|
|
if tokens[prevIndex].isStartOfScope, i != startIndex {
|
|
i = startIndex
|
|
} else {
|
|
i = prevIndex
|
|
}
|
|
continue
|
|
}
|
|
|
|
if case let .identifier(name) = tokens[prevIndex], name.first?.isUppercase == true {
|
|
switch last(.nonSpaceOrCommentOrLinebreak, before: prevIndex) {
|
|
case .identifier("some")?, .delimiter?, .startOfScope?, .endOfScope?,
|
|
.operator(_, .infix)?, .operator(_, .prefix)?, nil:
|
|
return true
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
|
|
if tokens[prevIndex].string == "#Preview" {
|
|
return true
|
|
}
|
|
|
|
if tokens[prevIndex].isStartOfScope, i != startIndex {
|
|
i = startIndex
|
|
} else {
|
|
i = prevIndex
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
/// Detect if identifier requires backtick escaping
|
|
func backticksRequired(at i: Int, ignoreLeadingDot: Bool = false) -> Bool {
|
|
guard let token = token(at: i), token.isIdentifier else {
|
|
return false
|
|
}
|
|
|
|
let unescaped = token.unescaped()
|
|
|
|
// This identifier may be a raw identifier like ``func `function name with spaces`()``.
|
|
// Validate that the escaped identifier is a valid standard identifier.
|
|
var scalarView = UnicodeScalarView(unescaped.unicodeScalars)
|
|
let parsedIdentifier = scalarView.parseIdentifier()
|
|
guard parsedIdentifier == .identifier(unescaped) || parsedIdentifier == .keyword(unescaped) else {
|
|
return true
|
|
}
|
|
|
|
if !unescaped.isSwiftKeyword {
|
|
switch unescaped {
|
|
case "_", "$":
|
|
return true
|
|
case "self":
|
|
if last(.nonSpaceOrCommentOrLinebreak, before: i)?.isOperator(".") == true {
|
|
return true
|
|
}
|
|
fallthrough
|
|
case "super", "nil", "true", "false":
|
|
if options.swiftVersion < "4" {
|
|
return true
|
|
}
|
|
case "Self", "Any":
|
|
if let prevToken = last(.nonSpaceOrCommentOrLinebreak, before: i),
|
|
[.delimiter(":"), .operator("->", .infix)].contains(prevToken)
|
|
{
|
|
// TODO: check for other cases where it's safe to use unescaped
|
|
return false
|
|
}
|
|
case "Type":
|
|
if currentScope(at: i) == .startOfScope("{") {
|
|
// TODO: check it's actually inside a type declaration, otherwise backticks aren't needed
|
|
return true
|
|
}
|
|
if last(.nonSpaceOrCommentOrLinebreak, before: i)?.isOperator(".") == true {
|
|
return true
|
|
}
|
|
return false
|
|
case "get", "set", "willSet", "didSet", "init", "_modify":
|
|
return isAccessorKeyword(at: i, checkKeyword: false)
|
|
case "actor":
|
|
if last(.nonSpaceOrCommentOrLinebreak, before: i)?.isOperator(ofType: .infix) == true {
|
|
return false
|
|
}
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
if let prevIndex = index(of: .nonSpaceOrCommentOrLinebreak, before: i, if: {
|
|
$0.isOperator(".")
|
|
}) {
|
|
if unescaped == "init" {
|
|
return true
|
|
}
|
|
if options.swiftVersion >= "5" || self.token(at: prevIndex - 1)?.isOperator("\\") != true {
|
|
return ignoreLeadingDot
|
|
}
|
|
return true
|
|
}
|
|
if index(of: .nonSpaceOrCommentOrLinebreak, before: i, if: { $0.isOperator("::") }) != nil {
|
|
// After :: (module selector), keywords are ordinary identifiers except for these
|
|
return ["deinit", "init", "subscript"].contains(unescaped)
|
|
}
|
|
guard !["let", "var"].contains(unescaped) else {
|
|
return true
|
|
}
|
|
return !isArgumentPosition(at: i)
|
|
}
|
|
|
|
/// Is token at argument position
|
|
func isArgumentPosition(at i: Int) -> Bool {
|
|
assert(tokens[i].isIdentifierOrKeyword)
|
|
guard let nextIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: i) else {
|
|
return false
|
|
}
|
|
let nextToken = tokens[nextIndex]
|
|
if nextToken == .delimiter(":") || (nextToken.isIdentifier &&
|
|
next(.nonSpaceOrCommentOrLinebreak, after: nextIndex) == .delimiter(":")),
|
|
currentScope(at: i) == .startOfScope("(")
|
|
{
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
/// Determine if the specified token is the start of a commented line of code
|
|
func isCommentedCode(at i: Int) -> Bool {
|
|
if token(at: i) == .startOfScope("//"), token(at: i - 1)?.isSpace != true {
|
|
switch token(at: i + 1) {
|
|
case nil, .linebreak?:
|
|
return true
|
|
case let .space(space)? where space.hasPrefix(options.indent):
|
|
return true
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
/// Returns true if the identifier at the specified index is a label
|
|
func isLabel(at i: Int) -> Bool {
|
|
guard case .identifier = token(at: i) else {
|
|
return false
|
|
}
|
|
return next(.nonSpaceOrCommentOrLinebreak, after: i) == .delimiter(":")
|
|
}
|
|
|
|
/// Returns true if the token at the specified index is the opening delimiter of a parameter list
|
|
/// (i.e. either the `(` for a function, or the `<` for some generic parameters)
|
|
func isParameterList(at i: Int) -> Bool {
|
|
assert([.startOfScope("("), .startOfScope("<")].contains(tokens[i]))
|
|
guard let endIndex = endOfScope(at: i),
|
|
let nextIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: endIndex)
|
|
else { return false }
|
|
switch tokens[nextIndex] {
|
|
case .operator("->", .infix), .keyword("throws"), .keyword("rethrows"):
|
|
return true
|
|
case .keyword("in"):
|
|
return last(.nonSpaceOrLinebreak, before: i) != .keyword("for")
|
|
case .identifier("async"):
|
|
if let nextToken = next(.nonSpaceOrCommentOrLinebreak, after: nextIndex),
|
|
[.operator("->", .infix), .keyword("throws"), .keyword("rethrows"),
|
|
.startOfScope("{")].contains(nextToken)
|
|
{
|
|
return true
|
|
}
|
|
default:
|
|
if let funcIndex = index(of: .keywordOrAttribute, before: i, if: {
|
|
[.keyword("func"), .keyword("init"), .keyword("subscript"), .keyword("macro")].contains($0)
|
|
}), lastIndex(of: .endOfScope("}"), in: funcIndex ..< i) == nil {
|
|
// Is parameters at start of function
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
/// Returns if the `case` keyword at the specified index is part of an enum (as opposed to `if case`)
|
|
func isEnumCase(at i: Int) -> Bool {
|
|
assert(tokens[i] == .keyword("case"))
|
|
switch last(.nonSpaceOrCommentOrLinebreak, before: i) {
|
|
case .identifier?, .endOfScope(")")?, .startOfScope("{")?:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
/// Parses a type name starting at the given index, of one of the following forms:
|
|
/// - `Foo`
|
|
/// - `[...]`
|
|
/// - `(...)`
|
|
/// - `Foo<...>`
|
|
/// - `(...) (async|throws|throws(Error)) -> ...`
|
|
/// - `...?`
|
|
/// - `...!`
|
|
/// - `any ...`
|
|
/// - `some ...`
|
|
/// - `borrowing ...`
|
|
/// - `consuming ...`
|
|
/// - `sending ...`
|
|
/// - `repeat ...`
|
|
/// - `each ...`
|
|
/// - `inout ...`
|
|
/// - `~...`
|
|
/// - any `@attribute ...`
|
|
/// - `(type).(type)`
|
|
/// - `(type) & (type)`
|
|
func parseType(
|
|
at startOfTypeIndex: Int,
|
|
excludeLowercaseIdentifiers: Bool = false,
|
|
excludeProtocolCompositions: Bool = false
|
|
) -> TypeName? {
|
|
guard let baseType = parseNonOptionalType(
|
|
at: startOfTypeIndex,
|
|
excludeLowercaseIdentifiers: excludeLowercaseIdentifiers,
|
|
excludeProtocolCompositions: excludeProtocolCompositions
|
|
)
|
|
else { return nil }
|
|
|
|
// Any type can be optional, so check for a trailing `?` or `!`.
|
|
// There cannot be any other tokens between the type and the operator:
|
|
//
|
|
// let foo: String? // allowed
|
|
// let foo: String???? // allowed
|
|
// let foo: String ? // not allowed
|
|
// let foo: String/*bar*/? // not allowed
|
|
//
|
|
if token(at: baseType.range.upperBound + 1)?.isUnwrapOperator == true {
|
|
var endOfOptionalType = baseType.range.upperBound + 1
|
|
|
|
while token(at: endOfOptionalType + 1)?.isUnwrapOperator == true {
|
|
endOfOptionalType += 1
|
|
}
|
|
|
|
return TypeName(range: baseType.range.lowerBound ... endOfOptionalType, formatter: self)
|
|
}
|
|
|
|
// Any type can be followed by a `.` or `&` which can then continue the type
|
|
let continuationOperators: [Token]
|
|
if excludeProtocolCompositions {
|
|
continuationOperators = [.operator(".", .infix)]
|
|
} else {
|
|
continuationOperators = [.operator(".", .infix), .operator("&", .infix)]
|
|
}
|
|
|
|
if let nextTokenIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: baseType.range.upperBound),
|
|
continuationOperators.contains(tokens[nextTokenIndex]),
|
|
let followingToken = index(of: .nonSpaceOrCommentOrLinebreak, after: nextTokenIndex),
|
|
let followingType = parseType(at: followingToken, excludeLowercaseIdentifiers: excludeLowercaseIdentifiers)
|
|
{
|
|
return TypeName(range: startOfTypeIndex ... followingType.range.upperBound, formatter: self)
|
|
}
|
|
|
|
// `::` can also continue a type (e.g. `Module::Type`), but unlike `.` and `&`,
|
|
// there must be no newline between `::` and the following identifier.
|
|
if let nextTokenIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: baseType.range.upperBound),
|
|
tokens[nextTokenIndex] == .operator("::", .infix),
|
|
let afterDoubleColonIndex = index(of: .nonSpaceOrComment, after: nextTokenIndex),
|
|
!tokens[afterDoubleColonIndex].isLinebreak,
|
|
let followingType = parseType(at: afterDoubleColonIndex, excludeLowercaseIdentifiers: excludeLowercaseIdentifiers)
|
|
{
|
|
return TypeName(range: startOfTypeIndex ... followingType.range.upperBound, formatter: self)
|
|
}
|
|
|
|
return baseType
|
|
}
|
|
|
|
private func parseNonOptionalType(
|
|
at startOfTypeIndex: Int,
|
|
excludeLowercaseIdentifiers: Bool,
|
|
excludeProtocolCompositions: Bool
|
|
) -> TypeName? {
|
|
let startToken = tokens[startOfTypeIndex]
|
|
|
|
/// Helpers that calls `parseType` with all of the optional params passed in by default
|
|
func parseType(at index: Int) -> TypeName? {
|
|
self.parseType(
|
|
at: index,
|
|
excludeLowercaseIdentifiers: excludeLowercaseIdentifiers,
|
|
excludeProtocolCompositions: excludeProtocolCompositions
|
|
)
|
|
}
|
|
|
|
// Parse types of the form `[...]`
|
|
if startToken == .startOfScope("["), let endOfScope = endOfScope(at: startOfTypeIndex) {
|
|
// Validate that the inner type is also valid
|
|
guard let innerTypeStartIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: startOfTypeIndex),
|
|
let innerType = parseType(at: innerTypeStartIndex),
|
|
let indexAfterType = index(of: .nonSpaceOrCommentOrLinebreak, after: innerType.range.upperBound)
|
|
else { return nil }
|
|
|
|
// This is either an array type of the form `[Element]`,
|
|
// or a dictionary type of the form `[Key: Value]`.
|
|
if indexAfterType != endOfScope {
|
|
guard tokens[indexAfterType] == .delimiter(":"),
|
|
let secondTypeIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: indexAfterType),
|
|
let secondType = parseType(at: secondTypeIndex),
|
|
let indexAfterSecondType = index(of: .nonSpaceOrCommentOrLinebreak, after: secondType.range.upperBound),
|
|
indexAfterSecondType == endOfScope
|
|
else { return nil }
|
|
}
|
|
|
|
return TypeName(range: startOfTypeIndex ... endOfScope, formatter: self)
|
|
}
|
|
|
|
// Parse types of the form `(...)` or `(...) -> ...`
|
|
if startToken == .startOfScope("("), let endOfScope = endOfScope(at: startOfTypeIndex) {
|
|
// Parse types of the form `(...) (async|throws|throws(Error)) -> ...`.
|
|
// Look for the `->` token, skipping over any `async`, `throws`, or `throws(Error)`s.
|
|
var searchIndex = endOfScope
|
|
|
|
if let effectsRange = parseFunctionDeclarationEffectsClause(at: endOfScope)?.range {
|
|
searchIndex = effectsRange.upperBound
|
|
}
|
|
|
|
// If we find a return arrow, this is a closure with a return type.
|
|
if let closureReturnIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: searchIndex),
|
|
tokens[closureReturnIndex] == .operator("->", .infix),
|
|
let returnTypeIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: closureReturnIndex),
|
|
let returnTypeRange = parseType(at: returnTypeIndex)?.range
|
|
{
|
|
return TypeName(range: startOfTypeIndex ... returnTypeRange.upperBound, formatter: self)
|
|
}
|
|
|
|
// If we find an expression-only keyword (like `as`, `is`, `try`) then this is
|
|
// an expression, not a type. But we allow function-type keywords like `throws`,
|
|
// `rethrows`, `async` that can appear in nested closure types like `(() throws -> Void)`
|
|
let expressionKeywords = Set(["as", "is", "try", "await", "if", "switch", "for", "while", "repeat", "guard", "in", "return", "throw"])
|
|
if tokens[startOfTypeIndex ... endOfScope].contains(where: {
|
|
$0.isKeyword && expressionKeywords.contains($0.string)
|
|
}) {
|
|
return nil
|
|
}
|
|
|
|
// Otherwise this is just `(...)`
|
|
return TypeName(range: startOfTypeIndex ... endOfScope, formatter: self)
|
|
}
|
|
|
|
// Parse types of the form `Foo<...>`
|
|
if let nextTokenIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: startOfTypeIndex),
|
|
tokens[nextTokenIndex] == .startOfScope("<"),
|
|
let endOfScope = endOfScope(at: nextTokenIndex)
|
|
{
|
|
return TypeName(range: startOfTypeIndex ... endOfScope, formatter: self)
|
|
}
|
|
|
|
// Parse types with any of the following prefixes, along with any `@attribute`.
|
|
let typePrefixes = Set(["any", "some", "borrowing", "consuming", "sending", "repeat", "each", "~", "inout"])
|
|
if typePrefixes.contains(startToken.string) || startToken.isAttribute,
|
|
let nextToken = index(of: .nonSpaceOrCommentOrLinebreak, after: startOfTypeIndex),
|
|
let followingType = parseType(at: nextToken)
|
|
{
|
|
return TypeName(range: startOfTypeIndex ... followingType.range.upperBound, formatter: self)
|
|
}
|
|
|
|
// Otherwise this is just a single identifier
|
|
if case let .identifier(name) = startToken, name != "init" {
|
|
let firstCharacter = name.drop { $0 == "_" || $0 == "`" }.first.flatMap(String.init) ?? ""
|
|
let isLowercaseIdentifier = firstCharacter.uppercased() != firstCharacter
|
|
guard !excludeLowercaseIdentifiers || !isLowercaseIdentifier else { return nil }
|
|
|
|
return TypeName(range: startOfTypeIndex ... startOfTypeIndex, formatter: self)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
/// Whether or not the token at this index could potentially be the last token in a type.
|
|
/// For a full list of all supported type patterns, check the documentation of `parseType(at:)`.
|
|
func isValidEndOfType(at index: Int) -> Bool {
|
|
if tokens[index].isIdentifier {
|
|
return true
|
|
}
|
|
|
|
let validEndOfTypeTokens = ["]", ")", ">", "?", "!"]
|
|
if validEndOfTypeTokens.contains(tokens[index].string) {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
/// Parses the expression starting at the given index.
|
|
///
|
|
/// A full list of expression types are available here:
|
|
/// https://docs.swift.org/swift-book/documentation/the-swift-programming-language/expressions/
|
|
///
|
|
/// Can be any of:
|
|
/// - `identifier`
|
|
/// - `1` (integer literal)
|
|
/// - `1.0` (double literal)
|
|
/// - `"foo"` (string literal)
|
|
/// - `(...)` (tuple)
|
|
/// - `[...]` (array or dictionary)
|
|
/// - `{ ... }` (closure)
|
|
/// - `#selector(...)` / macro invocations
|
|
/// - An `if/switch` expression (only allowed if this is the only expression in
|
|
/// a code block or if following an assignment `=` operator).
|
|
/// - Any value can be preceded by a prefix operator
|
|
/// - Any value can be preceded by `try`, `try?`, `try!`, or `await`
|
|
/// - Any value can be followed by a postfix operator
|
|
/// - Any value can be followed by an infix operator plus a right-hand-side expression.
|
|
/// - Any value can be followed by an arbitrary number of method calls `(...)`, subscripts `[...]`, or generic arguments `<...>`.
|
|
/// - Any value can be followed by a `.identifier`
|
|
func parseExpressionRange(
|
|
startingAt startIndex: Int,
|
|
allowConditionalExpressions: Bool = false
|
|
) -> ClosedRange<Int>? {
|
|
// Any expression can start with a prefix operator, or `await`, `repeat`, `each`
|
|
let prefixKeywords = ["await", "repeat", "each"]
|
|
if tokens[startIndex].isOperator(ofType: .prefix) || prefixKeywords.contains(tokens[startIndex].string),
|
|
let nextTokenIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: startIndex),
|
|
let followingExpression = parseExpressionRange(startingAt: nextTokenIndex, allowConditionalExpressions: allowConditionalExpressions)
|
|
{
|
|
return startIndex ... followingExpression.upperBound
|
|
}
|
|
|
|
// Any value can be preceded by `try`
|
|
if tokens[startIndex].string == "try" {
|
|
guard var nextTokenAfterTry = index(of: .nonSpaceOrCommentOrLinebreak, after: startIndex) else { return nil }
|
|
|
|
// `try` can either be by itself, or followed by `?` or `!` (`try`, `try?`, or `try!`).
|
|
// If present, skip the operator.
|
|
if tokens[nextTokenAfterTry].isUnwrapOperator {
|
|
guard let nextTokenAfterTryOperator = index(of: .nonSpaceOrCommentOrLinebreak, after: nextTokenAfterTry) else { return nil }
|
|
nextTokenAfterTry = nextTokenAfterTryOperator
|
|
}
|
|
|
|
if let followingExpression = parseExpressionRange(startingAt: nextTokenAfterTry, allowConditionalExpressions: allowConditionalExpressions) {
|
|
return startIndex ... followingExpression.upperBound
|
|
}
|
|
}
|
|
|
|
// Parse the base of any potential method call or chain,
|
|
// which is always a simple identifier or a simple literal.
|
|
var endOfExpression: Int
|
|
switch tokens[startIndex] {
|
|
case .identifier, .number:
|
|
endOfExpression = startIndex
|
|
|
|
case let .startOfScope(name):
|
|
// All types of scopes (tuples, arrays, closures, strings) are considered expressions
|
|
// _except_ for conditional complication blocks.
|
|
if ["#if", "#elseif", "#else"].contains(name) {
|
|
return nil
|
|
}
|
|
|
|
guard let endOfScope = endOfScope(at: startIndex) else { return nil }
|
|
endOfExpression = endOfScope
|
|
|
|
case let .keyword(keyword) where keyword.isMacroOrCompilerDirective:
|
|
// #selector() and macro expansions like #macro() are parsed into keyword tokens.
|
|
endOfExpression = startIndex
|
|
|
|
case .keyword("if"), .keyword("switch"):
|
|
guard allowConditionalExpressions,
|
|
let conditionalBranches = conditionalBranches(at: startIndex),
|
|
let lastBranch = conditionalBranches.last
|
|
else { return nil }
|
|
endOfExpression = lastBranch.endOfBranch
|
|
|
|
default:
|
|
return nil
|
|
}
|
|
|
|
while let nextTokenIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: endOfExpression),
|
|
let nextToken = token(at: nextTokenIndex)
|
|
{
|
|
switch nextToken {
|
|
// Any expression can be followed by an arbitrary number of method calls `(...)`, subscripts `[...]`, or generic arguments `<...>`.
|
|
case .startOfScope("("), .startOfScope("["), .startOfScope("<"):
|
|
// If there's a linebreak between an expression and a paren or subscript,
|
|
// then it's not parsed as a method call and is actually a separate expression
|
|
if tokens[endOfExpression ..< nextTokenIndex].contains(where: \.isLinebreak) {
|
|
return startIndex ... endOfExpression
|
|
}
|
|
|
|
guard let endOfScope = endOfScope(at: nextTokenIndex) else { return nil }
|
|
endOfExpression = endOfScope
|
|
|
|
// Any value can be followed by a `.identifier`
|
|
case .delimiter("."), .operator(".", _):
|
|
guard let nextIdentifierIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: nextTokenIndex),
|
|
tokens[nextIdentifierIndex].isIdentifier
|
|
else { return startIndex ... endOfExpression }
|
|
|
|
endOfExpression = nextIdentifierIndex
|
|
|
|
// Any value can be followed by a postfix operator
|
|
case .operator(_, .postfix):
|
|
endOfExpression = nextTokenIndex
|
|
|
|
// Any value can be followed by an infix operator, plus another expression
|
|
// - However, the assignment operator (`=`) is special and _isn't_ an expression
|
|
case let .operator(operatorString, .infix) where operatorString != "=":
|
|
guard let nextTokenIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: nextTokenIndex),
|
|
let nextExpression = parseExpressionRange(startingAt: nextTokenIndex)
|
|
else { return startIndex ... endOfExpression }
|
|
|
|
endOfExpression = nextExpression.upperBound
|
|
|
|
// Any value can be followed by `is`, `as`, `as?`, or `as?`, plus another expression
|
|
case .keyword("is"), .keyword("as"):
|
|
guard var nextTokenAfterKeyword = index(of: .nonSpaceOrCommentOrLinebreak, after: nextTokenIndex) else { return nil }
|
|
|
|
// `as` can either be by itself, or followed by `?` or `!` (`as`, `as?`, or `as!`).
|
|
// If present, skip the operator.
|
|
if tokens[nextTokenAfterKeyword].isUnwrapOperator {
|
|
guard let nextTokenAfterOperator = index(of: .nonSpaceOrCommentOrLinebreak, after: nextTokenAfterKeyword) else { return nil }
|
|
nextTokenAfterKeyword = nextTokenAfterOperator
|
|
}
|
|
|
|
guard let followingExpression = parseExpressionRange(startingAt: nextTokenAfterKeyword) else {
|
|
return startIndex ... endOfExpression
|
|
}
|
|
|
|
endOfExpression = followingExpression.upperBound
|
|
|
|
// Any value can be followed by a trailing closure
|
|
case .startOfScope("{") where isStartOfClosure(at: nextTokenIndex):
|
|
guard let endOfScope = endOfScope(at: nextTokenIndex) else { return nil }
|
|
|
|
// Within a conditional statement, an open brace is most likely
|
|
// to instead represent the body of the condition.
|
|
if isConditionalStatement(at: startIndex) {
|
|
return startIndex ... endOfExpression
|
|
}
|
|
|
|
endOfExpression = endOfScope
|
|
|
|
// Some values can be followed by a labeled trailing closure,
|
|
// like (expression) trailingClosure: { ... }
|
|
case .identifier:
|
|
guard let colonIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: nextTokenIndex),
|
|
tokens[colonIndex] == .delimiter(":"),
|
|
let startOfClosureIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: colonIndex),
|
|
tokens[startOfClosureIndex] == .startOfScope("{"),
|
|
let endOfClosureScope = endOfScope(at: startOfClosureIndex)
|
|
else { return startIndex ... endOfExpression }
|
|
|
|
endOfExpression = endOfClosureScope
|
|
|
|
default:
|
|
return startIndex ... endOfExpression
|
|
}
|
|
}
|
|
|
|
return startIndex ... endOfExpression
|
|
}
|
|
|
|
/// Parses the expression ending at the given index.
|
|
///
|
|
/// This is the reverse counterpart to `parseExpressionRange(startingAt:)`.
|
|
/// It works backwards from an ending position to find where the expression starts.
|
|
func parseExpressionRange(
|
|
endingAt endIndex: Int
|
|
) -> ClosedRange<Int>? {
|
|
let token = tokens[endIndex]
|
|
|
|
// First, check what comes BEFORE this token to see if we're part of a larger expression
|
|
if let prevIndex = index(of: .nonSpaceOrCommentOrLinebreak, before: endIndex) {
|
|
let prevToken = tokens[prevIndex]
|
|
|
|
// Check for dot notation (foo.bar)
|
|
if prevToken == .operator(".", .infix) || prevToken == .delimiter(".") {
|
|
guard let beforeDotIndex = index(of: .nonSpaceOrCommentOrLinebreak, before: prevIndex),
|
|
let previousExpression = parseExpressionRange(endingAt: beforeDotIndex)
|
|
else { return nil }
|
|
return previousExpression.lowerBound ... endIndex
|
|
}
|
|
|
|
// Check for infix operators (foo + bar, but not foo = bar)
|
|
if prevToken.isOperator(ofType: .infix), prevToken.string != "=" {
|
|
guard let beforeOperatorIndex = index(of: .nonSpaceOrCommentOrLinebreak, before: prevIndex),
|
|
let previousExpression = parseExpressionRange(endingAt: beforeOperatorIndex)
|
|
else { return nil }
|
|
return previousExpression.lowerBound ... endIndex
|
|
}
|
|
|
|
// Prefix operators have to be the start of a subexpression,
|
|
// but can come after infix operators like `foo == !bar`.
|
|
if prevToken.isOperator(ofType: .prefix),
|
|
let tokenBeforeOperator = index(of: .nonSpaceOrComment, before: prevIndex),
|
|
tokens[tokenBeforeOperator].isOperator(ofType: .infix),
|
|
let previousExpression = parseExpressionRange(endingAt: prevIndex)
|
|
{
|
|
return previousExpression.lowerBound ... endIndex
|
|
}
|
|
|
|
// Check for type casting keywords (as, is)
|
|
if prevToken == .keyword("as") || prevToken == .keyword("is") {
|
|
guard let beforeKeywordIndex = index(of: .nonSpaceOrCommentOrLinebreak, before: prevIndex),
|
|
let previousExpression = parseExpressionRange(endingAt: beforeKeywordIndex)
|
|
else { return nil }
|
|
return previousExpression.lowerBound ... endIndex
|
|
}
|
|
|
|
// Check for as?, as! (unwrap operator after "as")
|
|
if prevToken.isUnwrapOperator,
|
|
let asIndex = index(of: .nonSpaceOrCommentOrLinebreak, before: prevIndex),
|
|
tokens[asIndex] == .keyword("as")
|
|
{
|
|
guard let beforeAsIndex = index(of: .nonSpaceOrCommentOrLinebreak, before: asIndex),
|
|
let previousExpression = parseExpressionRange(endingAt: beforeAsIndex)
|
|
else { return nil }
|
|
return previousExpression.lowerBound ... endIndex
|
|
}
|
|
|
|
// Check for prefix operators or keywords (!, try, await)
|
|
let prefixKeywords = ["await", "try", "repeat", "each"]
|
|
if prevToken.isOperator(ofType: .prefix) || prefixKeywords.contains(prevToken.string) {
|
|
return prevIndex ... endIndex
|
|
}
|
|
|
|
// Check for operators after prefix keywords (try!, try?, await!)
|
|
if prevToken.isUnwrapOperator || prevToken == .operator("?", .postfix) {
|
|
if let beforeOperatorIndex = index(of: .nonSpaceOrCommentOrLinebreak, before: prevIndex),
|
|
prefixKeywords.contains(tokens[beforeOperatorIndex].string)
|
|
{
|
|
return beforeOperatorIndex ... endIndex
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle postfix and infix operators (!, ?, +) that can be preceded by other parts of the expression
|
|
if token.isOperator(ofType: .postfix) || token.isOperator(ofType: .infix) {
|
|
guard let prevIndex = index(of: .nonSpaceOrCommentOrLinebreak, before: endIndex),
|
|
let previousExpression = parseExpressionRange(endingAt: prevIndex)
|
|
else { return nil }
|
|
return previousExpression.lowerBound ... endIndex
|
|
}
|
|
|
|
// Handle end of scope tokens ), ], }, "
|
|
if case .endOfScope = token {
|
|
guard let startOfScope = index(of: .startOfScope, before: endIndex) else { return nil }
|
|
|
|
// Check if there's something before the scope (method call, subscript)
|
|
if let prevIndex = index(of: .nonSpaceOrCommentOrLinebreak, before: startOfScope),
|
|
let previousExpression = parseExpressionRange(endingAt: prevIndex)
|
|
{
|
|
return previousExpression.lowerBound ... endIndex
|
|
}
|
|
|
|
// The scope itself is the expression (array literal, closure, etc)
|
|
return startOfScope ... endIndex
|
|
}
|
|
|
|
// Base cases: identifiers, numbers, literals, etc.
|
|
if token.isIdentifier || token.isNumber {
|
|
return endIndex ... endIndex
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
/// Parses the expression that contains the token at the given index.
|
|
func parseExpressionRange(
|
|
containing index: Int
|
|
) -> ClosedRange<Int>? {
|
|
// To find the complete expression, parse forwards from the input index to the end of the expression,
|
|
// and then parse backwards from the end of that expression to the start of the complete expression.
|
|
var forwardRange = parseExpressionRange(startingAt: index)
|
|
|
|
// If this is an operator that can't ever be the start of an expression, parse from some previous token
|
|
if forwardRange == nil,
|
|
tokens[index].isOperator(ofType: .postfix) || tokens[index].isOperator(ofType: .infix)
|
|
{
|
|
var parseToken = index
|
|
while let previousToken = self.index(of: .nonSpaceOrCommentOrLinebreak, before: parseToken) {
|
|
// Check if this previous token is the start of a subexpression that contains the given index
|
|
if let forwardRangeFromPreviousToken = parseExpressionRange(startingAt: parseToken),
|
|
forwardRangeFromPreviousToken.contains(index)
|
|
{
|
|
forwardRange = forwardRangeFromPreviousToken
|
|
break
|
|
}
|
|
|
|
parseToken = previousToken
|
|
}
|
|
forwardRange = parseExpressionRange(startingAt: parseToken)
|
|
}
|
|
|
|
guard let forwardRange,
|
|
let backwardRange = parseExpressionRange(endingAt: forwardRange.upperBound),
|
|
backwardRange.contains(index)
|
|
else { return nil }
|
|
|
|
return backwardRange
|
|
}
|
|
|
|
/// Parses all of the declarations in the source file.
|
|
func parseDeclarations() -> [Declaration] {
|
|
parseDeclarations(in: tokens.indices)
|
|
}
|
|
|
|
/// Parses the declarations in the given range.
|
|
func parseDeclarations(in range: Range<Int>) -> [Declaration] {
|
|
parseDeclarations(in: range, _useForEachToken: true)
|
|
}
|
|
|
|
/// Parses the declarations in the given range.
|
|
///
|
|
/// Uses `forEachToken` to iterate through the tokens in the given range.
|
|
/// This enables declarations to read the `isEnabled` state from comment directives.
|
|
/// This can be disabled with `_useForEachToken: false` to avoid reentrancy if you
|
|
/// need to call `parseDeclarations` from within an existing `forEachToken` call.
|
|
private func parseDeclarations(in range: Range<Int>, _useForEachToken: Bool) -> [Declaration] {
|
|
// A temporary declaration value. We can't create a `DeclarationV2` directly
|
|
// within the `forEachToken` call, since `forEachToken` doesn't support reentrancy.
|
|
struct _Declaration {
|
|
let keyword: String
|
|
let keywordIndex: Int
|
|
let range: ClosedRange<Int>
|
|
}
|
|
|
|
var declarations = [_Declaration]()
|
|
var startOfDeclaration = range.lowerBound
|
|
let startOfScopeAtDeclaration = startOfScope(at: startOfDeclaration)
|
|
|
|
let handleIndex = { [self] (index: Int, token: Token) in
|
|
guard range.contains(index),
|
|
index >= startOfDeclaration,
|
|
token.isDeclarationTypeKeyword || token == .startOfScope("#if"),
|
|
startOfScopeAtDeclaration == startOfScope(at: index)
|
|
else {
|
|
return
|
|
}
|
|
|
|
let keywordIndex = index
|
|
let declarationKeyword = declarationType(at: keywordIndex) ?? "#if"
|
|
let endOfDeclaration = _endOfDeclarationInTypeBody(atDeclarationKeyword: keywordIndex)
|
|
|
|
let declarationRange = startOfDeclaration ... min(endOfDeclaration ?? .max, range.upperBound - 1)
|
|
startOfDeclaration = declarationRange.upperBound + 1
|
|
|
|
// If the current rule is disabled at this index, don't keep the declaration.
|
|
// This makes it easy for parseDeclarations-based rules to support directives
|
|
// like disable and disable:next.
|
|
if isEnabled {
|
|
declarations.append(_Declaration(
|
|
keyword: declarationKeyword,
|
|
keywordIndex: keywordIndex,
|
|
range: declarationRange
|
|
))
|
|
}
|
|
}
|
|
|
|
if _useForEachToken {
|
|
forEachToken(onlyWhereEnabled: false) { index, token in
|
|
handleIndex(index, token)
|
|
}
|
|
} else {
|
|
for (index, token) in tokens.enumerated() {
|
|
handleIndex(index, token)
|
|
}
|
|
}
|
|
|
|
return declarations.map { declaration in
|
|
// If this declaration represents a type, we need to parse its inner declarations as well.
|
|
if Token.swiftTypeKeywords.contains(declaration.keyword),
|
|
let bodyOpenBrace = self.index(of: .startOfScope("{"), after: declaration.keywordIndex),
|
|
let endOfScope = endOfScope(at: bodyOpenBrace)
|
|
{
|
|
// The type body excludes any leading linebreaks or trailing spaces.
|
|
let body: [Declaration]
|
|
if let startOfBody = self.index(of: .nonLinebreak, after: bodyOpenBrace),
|
|
let endOfBody = self.index(of: .nonSpace, before: endOfScope),
|
|
startOfBody <= endOfBody
|
|
{
|
|
body = parseDeclarations(in: Range(startOfBody ... endOfBody))
|
|
} else {
|
|
body = parseDeclarations(in: (bodyOpenBrace + 1) ..< endOfScope)
|
|
}
|
|
|
|
return TypeDeclaration(
|
|
keyword: declaration.keyword,
|
|
range: declaration.range,
|
|
body: body,
|
|
formatter: self
|
|
)
|
|
}
|
|
|
|
// If this declaration represents a conditional compilation block,
|
|
// we also have to parse its inner declarations.
|
|
else if declaration.keyword == "#if",
|
|
let endOfScope = endOfScope(at: declaration.keywordIndex)
|
|
{
|
|
// The conditional compilation body excludes any leading linebreaks or trailing spaces.
|
|
let body: [Declaration]
|
|
if let startOfBody = self.index(of: .nonLinebreak, after: endOfLine(at: declaration.keywordIndex)),
|
|
let endOfBody = self.index(of: .nonSpace, before: endOfScope),
|
|
startOfBody <= endOfBody
|
|
{
|
|
body = parseDeclarations(in: Range(startOfBody ... endOfBody))
|
|
} else {
|
|
body = parseDeclarations(in: (endOfLine(at: declaration.keywordIndex) + 1) ..< endOfScope)
|
|
}
|
|
|
|
return ConditionalCompilationDeclaration(
|
|
range: declaration.range,
|
|
body: body,
|
|
formatter: self
|
|
)
|
|
}
|
|
|
|
else {
|
|
return SimpleDeclaration(
|
|
keyword: declaration.keyword,
|
|
range: declaration.range,
|
|
formatter: self
|
|
)
|
|
}
|
|
}
|
|
.filter(\.isValid)
|
|
}
|
|
|
|
/// Returns the end index of the `Declaration` containing `declarationKeywordIndex`.
|
|
/// This is mostly an implementation detail of `parseDeclarations` and only correctly
|
|
/// handles declarations within type bodies (not within function bodies).
|
|
/// - `declarationKeywordIndex.isDeclarationTypeKeyword` must be `true`
|
|
/// (e.g. it must be a keyword like `let`, `var`, `func`, `class`, etc.
|
|
func _endOfDeclarationInTypeBody(atDeclarationKeyword declarationKeywordIndex: Int) -> Int? {
|
|
assert(tokens[declarationKeywordIndex].isDeclarationTypeKeyword
|
|
|| tokens[declarationKeywordIndex] == .startOfScope("#if"))
|
|
|
|
// Get declaration keyword
|
|
var searchIndex = declarationKeywordIndex
|
|
let declarationKeyword = declarationType(at: declarationKeywordIndex) ?? "#if"
|
|
var endOfDeclaration: Int?
|
|
switch tokens[declarationKeywordIndex] {
|
|
case .startOfScope("#if"):
|
|
// For conditional compilation blocks, the `declarationKeyword` _is_ the `startOfScope`
|
|
// so we can immediately skip to the corresponding #endif
|
|
if let endOfConditionalCompilationScope = endOfScope(at: declarationKeywordIndex) {
|
|
searchIndex = endOfConditionalCompilationScope
|
|
}
|
|
case .keyword("class") where declarationKeyword != "class":
|
|
// Most declarations will include exactly one token that `isDeclarationTypeKeyword` in
|
|
// - `class func` methods will have two (and the first one will be incorrect!)
|
|
searchIndex = index(of: .keyword(declarationKeyword), after: declarationKeywordIndex) ?? searchIndex
|
|
case .keyword("import"):
|
|
// Symbol imports (like `import class Module.Type`) will have an extra `isDeclarationTypeKeyword`
|
|
// immediately following their `declarationKeyword`, so we need to skip them.
|
|
if let symbolTypeKeywordIndex = index(of: .nonSpaceOrComment, after: declarationKeywordIndex),
|
|
tokens[symbolTypeKeywordIndex].isDeclarationTypeKeyword
|
|
{
|
|
searchIndex = symbolTypeKeywordIndex
|
|
}
|
|
case .keyword("protocol"), .keyword("struct"), .keyword("actor"),
|
|
.keyword("enum"), .keyword("extension"):
|
|
if let scopeStart = index(of: .startOfScope("{"), after: declarationKeywordIndex) {
|
|
searchIndex = endOfScope(at: scopeStart) ?? searchIndex
|
|
}
|
|
case .keyword("let"), .keyword("var"):
|
|
if let propertyDeclaration = parsePropertyDeclaration(atIntroducerIndex: declarationKeywordIndex) {
|
|
searchIndex = propertyDeclaration.range.upperBound
|
|
endOfDeclaration = propertyDeclaration.range.upperBound
|
|
}
|
|
case .keyword("func"), .keyword("subscript"), .keyword("init"):
|
|
if let functionDeclaration = parseFunctionDeclaration(keywordIndex: declarationKeywordIndex) {
|
|
searchIndex = functionDeclaration.range.upperBound
|
|
endOfDeclaration = functionDeclaration.range.upperBound
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
|
|
let nextDeclarationKeywordIndex = index(after: searchIndex, where: {
|
|
$0.isDeclarationTypeKeyword || $0 == .startOfScope("#if")
|
|
})
|
|
|
|
// If this is the last declaration in the type body, return nil to ensure we include all remaining tokens in the type body.
|
|
if nextDeclarationKeywordIndex == nil {
|
|
return nil
|
|
}
|
|
|
|
// Search for the next declaration so we know where this declaration ends
|
|
// (the token before the first token of the following declaration).
|
|
if let nextDeclarationKeywordIndex,
|
|
let lastIndexBeforeNextDeclaration = index(
|
|
before: startOfModifiers(at: nextDeclarationKeywordIndex, includingAttributes: true),
|
|
where: { !$0.isSpaceOrCommentOrLinebreak }
|
|
).map({ endOfLine(at: $0) })
|
|
{
|
|
// If we have an existing `endOfDeclaration` index from a parsing implementation like
|
|
// `parsePropertyDeclaration` or `parseFunctionDeclaration`, prefer that index.
|
|
if let existingEndOfDeclarationValue = endOfDeclaration {
|
|
endOfDeclaration = max(existingEndOfDeclarationValue, lastIndexBeforeNextDeclaration)
|
|
} else {
|
|
endOfDeclaration = lastIndexBeforeNextDeclaration
|
|
}
|
|
}
|
|
|
|
// Prefer keeping linebreaks at the end of a declaration's tokens,
|
|
// instead of the start of the next declaration's tokens.
|
|
// - This includes any spaces on blank lines, but doesn't include the
|
|
// indentation associated with the next declaration.
|
|
while let linebreakSearchIndex = endOfDeclaration,
|
|
token(at: linebreakSearchIndex + 1)?.isSpaceOrLinebreak == true
|
|
{
|
|
// Only spaces between linebreaks (e.g. spaces on blank lines) are included
|
|
if token(at: linebreakSearchIndex + 1)?.isSpace == true {
|
|
guard token(at: linebreakSearchIndex)?.isLinebreak == true,
|
|
token(at: linebreakSearchIndex + 2)?.isLinebreak == true
|
|
else { break }
|
|
}
|
|
|
|
endOfDeclaration = linebreakSearchIndex + 1
|
|
}
|
|
|
|
return endOfDeclaration
|
|
}
|
|
|
|
/// Parses the inner-most type that contains the given index.
|
|
func parseEnclosingType(containing index: Int) -> TypeDeclaration? {
|
|
guard let startOfScope = startOfScope(at: index) else { return nil }
|
|
|
|
if let typeKeyword = indexOfLastSignificantKeyword(at: startOfScope, excluding: ["where"]),
|
|
Token.swiftTypeKeywords.contains(tokens[typeKeyword].string),
|
|
let bodyOpenBrace = self.index(of: .startOfScope("{"), after: typeKeyword),
|
|
let endOfScope = endOfScope(at: bodyOpenBrace)
|
|
{
|
|
// When parsing the body, use `_useForEachToken: false` to enable
|
|
// `parseEnclosingType` to be called from within `forEachToken` loops.
|
|
return TypeDeclaration(
|
|
keyword: tokens[typeKeyword].string,
|
|
range: startOfModifiers(at: typeKeyword, includingAttributes: true) ... endOfScope,
|
|
body: parseDeclarations(in: (bodyOpenBrace + 1) ..< endOfScope, _useForEachToken: false),
|
|
formatter: self
|
|
)
|
|
}
|
|
|
|
else {
|
|
return parseEnclosingType(containing: startOfScope)
|
|
}
|
|
}
|
|
|
|
/// Whether or not the body within this scope is a single expression
|
|
func scopeBodyIsSingleExpression(at startOfScopeIndex: Int) -> Bool {
|
|
guard let endOfScopeIndex = endOfScope(at: startOfScopeIndex),
|
|
startOfScopeIndex + 1 != endOfScopeIndex,
|
|
let firstTokenInBody = index(of: .nonSpaceOrCommentOrLinebreak, after: startOfScopeIndex + 1),
|
|
let expressionRange = parseExpressionRange(startingAt: firstTokenInBody, allowConditionalExpressions: true)
|
|
else { return false }
|
|
|
|
return index(of: .nonSpaceOrCommentOrLinebreak, after: expressionRange.upperBound) == endOfScopeIndex
|
|
}
|
|
|
|
/// Whether or not the comment starting at the given index is a doc comment
|
|
func isDocComment(startOfComment: Int) -> Bool {
|
|
let commentToken = tokens[startOfComment]
|
|
guard commentToken == .startOfScope("//") || commentToken == .startOfScope("/*") else {
|
|
return false
|
|
}
|
|
|
|
// Doc comment tokens like `///` and `/**` aren't parsed as a
|
|
// single `.startOfScope` token -- they're parsed as:
|
|
// `.startOfScope("//"), .commentBody("/ ...")` or
|
|
// `.startOfScope("/*"), .commentBody("* ...")`
|
|
let startOfDocCommentBody: String
|
|
switch commentToken.string {
|
|
case "//":
|
|
startOfDocCommentBody = "/"
|
|
case "/*":
|
|
startOfDocCommentBody = "*"
|
|
default:
|
|
return false
|
|
}
|
|
|
|
guard let commentBody = token(at: startOfComment + 1),
|
|
commentBody.isCommentBody
|
|
else { return false }
|
|
|
|
return commentBody.string.hasPrefix(startOfDocCommentBody)
|
|
}
|
|
|
|
struct ImportRange: Comparable {
|
|
var module: String
|
|
var range: Range<Int>
|
|
var attributes: [String]
|
|
var accessLevel: String?
|
|
|
|
var isTestable: Bool {
|
|
attributes.contains("@testable")
|
|
}
|
|
|
|
static func < (lhs: ImportRange, rhs: ImportRange) -> Bool {
|
|
let la = lhs.module.lowercased()
|
|
let lb = rhs.module.lowercased()
|
|
return la == lb ? lhs.module < rhs.module : la < lb
|
|
}
|
|
}
|
|
|
|
/// A property of the format `(let|var) identifier: Type = expression { ... }`.
|
|
/// - `: Type`, `= expression`, and the following `{ ... }` body are optional
|
|
struct PropertyDeclaration {
|
|
/// The start index for this property's list of modifiers.
|
|
/// If there are no modifiers, `startOfModifiersIndex` is just `introducerIndex`.
|
|
let startOfModifiersIndex: Int
|
|
|
|
/// The index of the `let` or `var` keyword
|
|
let introducerIndex: Int
|
|
|
|
/// The identifier / name of this property.
|
|
let identifier: String
|
|
|
|
/// The index of this property's identifier / name.
|
|
let identifierIndex: Int
|
|
|
|
/// The colon that precedes the type, if present.
|
|
let colonIndex: Int?
|
|
|
|
/// Information about the property's type definition, if written explicitly.
|
|
let type: TypeName?
|
|
|
|
/// Auto-updating range for the property's type.
|
|
let typeRange: AutoUpdatingRange?
|
|
|
|
/// Information about the value following the property's `=` token, if present.
|
|
let value: (assignmentIndex: Int, expressionRange: ClosedRange<Int>)?
|
|
|
|
/// Information about the body following the property, which can include
|
|
/// `get`, `set`, `willSet`, `didSet` blocks, or just a single getter.
|
|
/// - `scopeRange` is the range starting at the `{` token and ending at the `}` token.
|
|
/// - `range` is the range of tokens inside the body scope.
|
|
let body: (scopeRange: ClosedRange<Int>, range: ClosedRange<Int>)?
|
|
|
|
var range: ClosedRange<Int> {
|
|
if let bodyScopeRange = body?.scopeRange {
|
|
return startOfModifiersIndex ... bodyScopeRange.upperBound
|
|
} else if let value {
|
|
return startOfModifiersIndex ... value.expressionRange.upperBound
|
|
} else if let typeRange {
|
|
return startOfModifiersIndex ... typeRange.range.upperBound
|
|
} else {
|
|
return startOfModifiersIndex ... identifierIndex
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Parses a property of the format `(let|var) identifier: Type = expression`
|
|
/// starting at the given introducer index (the `let` / `var` keyword).
|
|
///
|
|
/// Does not attempt to parse less-common property declarations that define multiple identifiers,
|
|
/// like `let (foo, bar) = (1, 2)` or `let foo: Foo, bar: Bar`.
|
|
func parsePropertyDeclaration(atIntroducerIndex introducerIndex: Int) -> PropertyDeclaration? {
|
|
guard ["let", "var"].contains(tokens[introducerIndex].string),
|
|
let propertyIdentifierIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: introducerIndex),
|
|
let propertyIdentifier = token(at: propertyIdentifierIndex),
|
|
propertyIdentifier.isIdentifier
|
|
else { return nil }
|
|
|
|
var typeInformation: (colonIndex: Int, type: TypeName, range: AutoUpdatingRange)?
|
|
|
|
if let colonIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: propertyIdentifierIndex),
|
|
tokens[colonIndex] == .delimiter(":"),
|
|
let startOfTypeIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: colonIndex),
|
|
let type = parseType(at: startOfTypeIndex)
|
|
{
|
|
typeInformation = (
|
|
colonIndex: colonIndex,
|
|
type: type,
|
|
range: AutoUpdatingRange(range: type.range, formatter: self)
|
|
)
|
|
}
|
|
|
|
let endOfTypeOrIdentifier = typeInformation?.range.upperBound ?? propertyIdentifierIndex
|
|
var valueInformation: (assignmentIndex: Int, expressionRange: ClosedRange<Int>)?
|
|
|
|
if let assignmentIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: endOfTypeOrIdentifier),
|
|
tokens[assignmentIndex] == .operator("=", .infix),
|
|
let startOfExpression = index(of: .nonSpaceOrCommentOrLinebreak, after: assignmentIndex),
|
|
let expressionRange = parseExpressionRange(startingAt: startOfExpression, allowConditionalExpressions: true)
|
|
{
|
|
valueInformation = (
|
|
assignmentIndex: assignmentIndex,
|
|
expressionRange: expressionRange
|
|
)
|
|
}
|
|
|
|
let endOfTypeOrIdentifierOrValue = valueInformation?.expressionRange.upperBound ?? endOfTypeOrIdentifier
|
|
var body: (scopeRange: ClosedRange<Int>, range: ClosedRange<Int>)?
|
|
|
|
if let startOfBodyIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: endOfTypeOrIdentifierOrValue),
|
|
tokens[startOfBodyIndex] == .startOfScope("{"),
|
|
let endOfScope = endOfScope(at: startOfBodyIndex)
|
|
{
|
|
let bodyRange = startOfBodyIndex ... endOfScope
|
|
|
|
let rangeInsideBody: ClosedRange<Int>
|
|
if startOfBodyIndex + 1 != endOfScope {
|
|
if let firstTokenInBody = index(of: .nonSpaceOrCommentOrLinebreak, after: startOfBodyIndex + 1),
|
|
let lastTokenInBody = index(of: .nonSpaceOrCommentOrLinebreak, before: endOfScope),
|
|
firstTokenInBody <= lastTokenInBody
|
|
{
|
|
rangeInsideBody = firstTokenInBody ... lastTokenInBody
|
|
} else {
|
|
rangeInsideBody = startOfBodyIndex + 1 ... endOfScope - 1
|
|
}
|
|
} else {
|
|
rangeInsideBody = bodyRange
|
|
}
|
|
|
|
body = (bodyRange, rangeInsideBody)
|
|
}
|
|
|
|
return PropertyDeclaration(
|
|
startOfModifiersIndex: startOfModifiers(at: introducerIndex, includingAttributes: true),
|
|
introducerIndex: introducerIndex,
|
|
identifier: propertyIdentifier.string,
|
|
identifierIndex: propertyIdentifierIndex,
|
|
colonIndex: typeInformation?.colonIndex,
|
|
type: typeInformation?.type,
|
|
typeRange: typeInformation?.range,
|
|
value: valueInformation,
|
|
body: body
|
|
)
|
|
}
|
|
|
|
/// Shared import rules implementation
|
|
func parseImports() -> [[ImportRange]] {
|
|
var importStack = [[ImportRange]]()
|
|
var importRanges = [ImportRange]()
|
|
forEach(.keyword("import")) { i, _ in
|
|
func pushStack() {
|
|
importStack.append(importRanges)
|
|
importRanges.removeAll()
|
|
}
|
|
// Get start of line
|
|
var startIndex = index(of: .linebreak, before: i) ?? 0
|
|
// Check for attributes
|
|
var previousKeywordIndex = index(of: .keywordOrAttribute, before: i)
|
|
while let previousIndex = previousKeywordIndex {
|
|
var nextStart: Int? // workaround for Swift Linux bug
|
|
if tokens[previousIndex].isAttribute {
|
|
if previousIndex < startIndex {
|
|
nextStart = index(of: .linebreak, before: previousIndex) ?? 0
|
|
}
|
|
previousKeywordIndex = index(of: .keywordOrAttribute, before: previousIndex)
|
|
startIndex = nextStart ?? startIndex
|
|
} else if case let .keyword(kw) = tokens[previousIndex],
|
|
_FormatRules.aclModifiers.contains(kw)
|
|
{
|
|
// Allow import access modifiers (Swift 6 SE-0409)
|
|
previousKeywordIndex = index(of: .keywordOrAttribute, before: previousIndex)
|
|
} else if previousIndex >= startIndex {
|
|
return
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
// Gather comments
|
|
let codeStartIndex = startIndex
|
|
var prevIndex = index(of: .linebreak, before: startIndex) ?? 0
|
|
while startIndex > 0,
|
|
next(.nonSpace, after: prevIndex)?.isComment == true,
|
|
next(.nonSpaceOrComment, after: prevIndex)?.isLinebreak == true
|
|
{
|
|
if prevIndex == 0, index(of: .startOfScope("#if"), before: startIndex) != nil {
|
|
break
|
|
}
|
|
startIndex = prevIndex
|
|
prevIndex = index(of: .linebreak, before: startIndex) ?? 0
|
|
}
|
|
// Check if comment is potentially a file header
|
|
if last(.nonSpaceOrCommentOrLinebreak, before: startIndex) == nil {
|
|
for case let .commentBody(body) in tokens[startIndex ..< codeStartIndex] {
|
|
if body.contains("created") || body.contains("Created") ||
|
|
body.contains(options.fileInfo.fileName ?? ".swift") ||
|
|
body.commentDirective == "swift-tools-version"
|
|
{
|
|
startIndex = codeStartIndex
|
|
break
|
|
}
|
|
}
|
|
}
|
|
// Get end of line
|
|
let endIndex = index(of: .linebreak, after: i) ?? tokens.count
|
|
// Get name
|
|
if let firstPartIndex = index(of: .identifier, after: i) {
|
|
var name = tokens[firstPartIndex].string
|
|
var partIndex = firstPartIndex
|
|
loop: while let nextPartIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: partIndex) {
|
|
switch tokens[nextPartIndex] {
|
|
case .operator(".", .infix):
|
|
name += "."
|
|
case let .identifier(string) where name.hasSuffix("."):
|
|
name += string
|
|
default:
|
|
break loop
|
|
}
|
|
partIndex = nextPartIndex
|
|
}
|
|
let range = startIndex ..< endIndex as Range
|
|
let accessLevel: String? = tokens[range].lazy.compactMap { token -> String? in
|
|
guard case let .keyword(kw) = token, _FormatRules.aclModifiers.contains(kw) else { return nil }
|
|
return kw
|
|
}.first
|
|
importRanges.append(ImportRange(
|
|
module: name,
|
|
range: range,
|
|
attributes: tokens[range].compactMap { $0.isAttribute ? $0.string : nil },
|
|
accessLevel: accessLevel
|
|
))
|
|
} else {
|
|
// Error
|
|
pushStack()
|
|
return
|
|
}
|
|
if next(.spaceOrCommentOrLinebreak, after: endIndex)?.isLinebreak == true {
|
|
// Blank line after - consider this the end of a block
|
|
pushStack()
|
|
return
|
|
}
|
|
if var nextTokenIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: endIndex) {
|
|
while tokens[nextTokenIndex].isAttribute {
|
|
guard let nextIndex = index(of: .nonSpaceOrLinebreak, after: nextTokenIndex) else {
|
|
// End of imports
|
|
pushStack()
|
|
return
|
|
}
|
|
nextTokenIndex = nextIndex
|
|
}
|
|
let nextToken = tokens[nextTokenIndex]
|
|
let isImportKeyword = nextToken == .keyword("import")
|
|
// Access modifiers only continue the import block when they are immediately followed by import.
|
|
let isAccessModifierBeforeImport = nextToken.isKeyword &&
|
|
_FormatRules.aclModifiers.contains(nextToken.string) &&
|
|
next(.nonSpaceOrComment, after: nextTokenIndex) == .keyword("import")
|
|
if !isImportKeyword, !isAccessModifierBeforeImport {
|
|
// End of imports
|
|
pushStack()
|
|
return
|
|
}
|
|
}
|
|
}
|
|
// End of imports
|
|
importStack.append(importRanges)
|
|
return importStack
|
|
}
|
|
|
|
/// Whether or not the given module is imported in this file
|
|
func hasImport(_ moduleName: String) -> Bool {
|
|
parseImports().contains(where: { importGroup in
|
|
importGroup.contains(where: { importedModule in
|
|
importedModule.module == moduleName
|
|
})
|
|
})
|
|
}
|
|
|
|
enum TestingFramework {
|
|
case xcTest
|
|
case swiftTesting
|
|
}
|
|
|
|
/// Detects which testing framework is being used in the file
|
|
func detectTestingFramework() -> TestingFramework? {
|
|
let hasTestingImport = hasImport("Testing")
|
|
let hasXCTestImport = hasImport("XCTest")
|
|
|
|
// If both frameworks are imported, return nil (ambiguous)
|
|
if hasTestingImport, hasXCTestImport {
|
|
return nil
|
|
}
|
|
|
|
if hasTestingImport {
|
|
return .swiftTesting
|
|
} else if hasXCTestImport {
|
|
return .xcTest
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
/// Is this a test function?
|
|
func isTestCase(
|
|
at funcKeywordIndex: Int,
|
|
in functionDecl: FunctionDeclaration,
|
|
for testingFramework: TestingFramework
|
|
) -> Bool {
|
|
assert(token(at: funcKeywordIndex) == .keyword("func"))
|
|
switch testingFramework {
|
|
case .xcTest:
|
|
guard functionDecl.name?.starts(with: "test") == true,
|
|
functionDecl.returnType == nil,
|
|
functionDecl.arguments.isEmpty
|
|
else {
|
|
return false
|
|
}
|
|
return true
|
|
case .swiftTesting:
|
|
return modifiersForDeclaration(at: funcKeywordIndex, contains: "@Test")
|
|
}
|
|
}
|
|
|
|
/// Checks if a function name has a disabled test prefix.
|
|
/// Matches patterns like: disable_foo, disableTestFoo, disabled_test_foo, x_test, XtestFoo, _test, etc.
|
|
func hasDisabledPrefix(_ name: String) -> Bool {
|
|
// Functions starting with underscore are considered disabled
|
|
guard !name.hasPrefix("_") else { return true }
|
|
|
|
let disabledTestPrefixBases = ["disable", "disabled", "skip", "skipped", "x"]
|
|
let lowercasedName = name.lowercased()
|
|
return disabledTestPrefixBases.contains {
|
|
lowercasedName.hasPrefix($0 + "_") || lowercasedName.hasPrefix($0 + "test")
|
|
}
|
|
}
|
|
|
|
/// Determines if a type declaration is likely a simple test case suite.
|
|
func isSimpleTestSuite(_ typeDecl: TypeDeclaration, for testFramework: TestingFramework) -> Bool {
|
|
guard let name = typeDecl.name else { return false }
|
|
|
|
// Don't apply to classes likely to be subclassed, since these are unsafe to modify.
|
|
if isLikelyToBeSubclassed(typeDecl) {
|
|
return false
|
|
}
|
|
|
|
// Don't apply to types with parameterized initializers (not test suites)
|
|
let hasParameterizedInit = typeDecl.body.contains {
|
|
$0.keyword == "init" &&
|
|
parseFunctionDeclaration(keywordIndex: $0.keywordIndex)?.arguments.isEmpty == false
|
|
}
|
|
if hasParameterizedInit {
|
|
return false
|
|
}
|
|
|
|
// Valid test suffixes for identifying test types
|
|
let testSuffixes = ["Test", "Tests", "TestCase", "TestCases", "Suite"]
|
|
|
|
// Checks if a type has at least one function that looks like a test (no arguments, no return type).
|
|
lazy var hasTestLikeFunction = {
|
|
for member in typeDecl.body where member.keyword == "func" {
|
|
guard let functionDecl = parseFunctionDeclaration(keywordIndex: member.keywordIndex) else {
|
|
continue
|
|
}
|
|
|
|
// Check if it has test-like signature (no args, no return type)
|
|
if functionDecl.arguments.isEmpty, functionDecl.returnType == nil {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}()
|
|
|
|
switch testFramework {
|
|
case .xcTest:
|
|
// For XCTest, only process classes (not structs)
|
|
guard typeDecl.keyword == "class" else { return false }
|
|
|
|
let conformsToXCTestCase = typeDecl.conformances.contains { $0.conformance.string == "XCTestCase" }
|
|
let hasTestSuffix = testSuffixes.contains { name.hasSuffix($0) }
|
|
let hasOtherConformances = typeDecl.conformances.contains { $0.conformance.string != "XCTestCase" }
|
|
|
|
// If it has conformances other than XCTestCase, skip it entirely
|
|
// (methods could be protocol requirements)
|
|
if hasOtherConformances {
|
|
return false
|
|
}
|
|
|
|
// If it conforms to XCTestCase only, include it
|
|
if conformsToXCTestCase {
|
|
return true
|
|
}
|
|
|
|
// If it has a test suffix and no conformances, check if it has test-like functions
|
|
if hasTestSuffix, typeDecl.conformances.isEmpty {
|
|
return hasTestLikeFunction
|
|
}
|
|
|
|
// Otherwise, exclude it
|
|
return false
|
|
|
|
case .swiftTesting:
|
|
// For Swift Testing, apply to classes/structs with specific test suffixes
|
|
// but only if they have test-like functions
|
|
if testSuffixes.contains(where: { name.hasSuffix($0) }) {
|
|
return hasTestLikeFunction
|
|
}
|
|
return false
|
|
}
|
|
}
|
|
|
|
/// Determines if a type is likely to be subclassed based on naming, documentation, and actual usage.
|
|
/// Returns true if the type should not be marked as final or treated as a regular class/struct.
|
|
func isLikelyToBeSubclassed(_ typeDecl: TypeDeclaration) -> Bool {
|
|
guard let name = typeDecl.name else { return false }
|
|
|
|
// Check if name contains "Base" (common convention for base classes)
|
|
if name.contains("Base") {
|
|
return true
|
|
}
|
|
|
|
// Check if doc comment mentions base class or subclassing
|
|
if let docCommentRange = typeDecl.docCommentRange {
|
|
let subclassRelatedTerms = ["base", "subclass"]
|
|
let docComment = tokens[docCommentRange].string.lowercased()
|
|
for term in subclassRelatedTerms {
|
|
if docComment.contains(term) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if this class is actually subclassed in the file
|
|
if typeDecl.keyword == "class" {
|
|
let declarations = parseDeclarations()
|
|
var isSubclassed = false
|
|
declarations.forEachRecursiveDeclaration { declaration in
|
|
guard declaration.keyword == "class" else { return }
|
|
let conformances = parseConformancesOfType(atKeywordIndex: declaration.keywordIndex)
|
|
for conformance in conformances {
|
|
// Extract base class name from generic types like "Container<String>" -> "Container"
|
|
let baseClassName = conformance.conformance.tokens.first?.string ?? conformance.conformance.string
|
|
if baseClassName == name {
|
|
isSubclassed = true
|
|
}
|
|
}
|
|
}
|
|
if isSubclassed {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
/// Adds imports for the given list of modules to this file if not already present
|
|
func addImports(_ importsToAddIfNeeded: [String]) {
|
|
// Don't add imports in fragments
|
|
if options.fragment { return }
|
|
|
|
let importRanges = parseImports()
|
|
let currentImports = Set(importRanges.flatMap { $0.map(\.module) })
|
|
|
|
for importToAddIfNeeded in importsToAddIfNeeded {
|
|
guard !currentImports.contains(importToAddIfNeeded) else { continue }
|
|
|
|
let newImport: [Token] = [.keyword("import"), .space(" "), .identifier(importToAddIfNeeded)]
|
|
|
|
// If there are any existing imports, add the new import in the existing group
|
|
if let firstImportIndex = index(of: .keyword("import"), after: -1) {
|
|
let startOfFirstImport = startOfModifiers(at: firstImportIndex, includingAttributes: true)
|
|
insert(newImport + [linebreakToken(for: firstImportIndex)], at: startOfFirstImport)
|
|
}
|
|
|
|
// Otherwise if there are no imports:
|
|
// - Make sure to insert the comment after any header comment if present
|
|
// - Include a blank line after the import
|
|
else {
|
|
let insertionIndex: Int
|
|
if let headerCommentRange = headerCommentTokenRange(), !headerCommentRange.isEmpty {
|
|
insertionIndex = headerCommentRange.upperBound
|
|
} else {
|
|
insertionIndex = 0
|
|
}
|
|
|
|
let newImportWithBlankLine = newImport + [
|
|
linebreakToken(for: insertionIndex),
|
|
linebreakToken(for: insertionIndex),
|
|
]
|
|
|
|
insert(newImportWithBlankLine, at: insertionIndex)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Removes the import for the given module names if present
|
|
func removeImports(_ moduleNames: Set<String>) {
|
|
let imports = parseImports().flatMap { $0 }
|
|
let importsToRemove = imports.filter { moduleNames.contains($0.module) }
|
|
|
|
let importsToRemoveByFileOrder = importsToRemove.sorted(by: { lhs, rhs in
|
|
lhs.range.lowerBound < rhs.range.lowerBound
|
|
})
|
|
|
|
for importToRemove in importsToRemoveByFileOrder.reversed() {
|
|
removeTokens(in: importToRemove.range)
|
|
}
|
|
}
|
|
|
|
/// Parses the arguments of the closure whose open brace is at the given index.
|
|
/// Returns `nil` if this is an anonymous closure, or if there was an issue parsing the closure arguments.
|
|
/// - `{ foo in ... }` returns `argumentNames: ["foo"]`
|
|
/// - `{ foo, bar in ... }` returns `argumentNames: ["foo", "bar"]`
|
|
/// - `{ (foo: Foo, bar: Bar) in ... }` returns `argumentNames: ["foo", "bar"]`
|
|
func parseClosureArgumentList(at closureOpenBraceIndex: Int) -> (argumentNames: [String], inKeywordIndex: Int)? {
|
|
var argumentNames = [String]()
|
|
let inKeywordIndex: Int
|
|
|
|
// Check if this is a closure `{ value in ... }` clause
|
|
if let indexAfterOpenBrace = index(of: .nonSpaceOrCommentOrLinebreak, after: closureOpenBraceIndex),
|
|
tokens[indexAfterOpenBrace].isIdentifier
|
|
{
|
|
// Parse a list of argument names like `foo, bar, baaz` until the `in` keyword
|
|
var currentArgumentListIndex = indexAfterOpenBrace
|
|
while tokens[currentArgumentListIndex].isIdentifier {
|
|
argumentNames.append(tokens[currentArgumentListIndex].string)
|
|
|
|
guard let nextIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: currentArgumentListIndex) else {
|
|
return nil
|
|
}
|
|
|
|
// Skip over any commas
|
|
if tokens[nextIndex] == .delimiter(",") {
|
|
currentArgumentListIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: nextIndex) ?? nextIndex
|
|
} else {
|
|
currentArgumentListIndex = nextIndex
|
|
}
|
|
}
|
|
|
|
// Finally we expect there to be an `in` keyword
|
|
guard tokens[currentArgumentListIndex] == .keyword("in") else {
|
|
return nil
|
|
}
|
|
|
|
inKeywordIndex = currentArgumentListIndex
|
|
}
|
|
|
|
// Check if this is a closure `{ (value: ValueType) in ... }` clause
|
|
else if let indexAfterOpenBrace = index(of: .nonSpaceOrCommentOrLinebreak, after: closureOpenBraceIndex),
|
|
tokens[indexAfterOpenBrace] == .startOfScope("("),
|
|
let endOfArgumentsScopeIndex = endOfScope(at: indexAfterOpenBrace),
|
|
let firstTokenInArgumentsList = index(of: .nonSpaceOrCommentOrLinebreak, after: indexAfterOpenBrace),
|
|
let indexAfterArguments = index(of: .nonSpaceOrCommentOrLinebreak, after: endOfArgumentsScopeIndex),
|
|
tokens[indexAfterArguments] == .keyword("in")
|
|
{
|
|
inKeywordIndex = indexAfterArguments
|
|
|
|
// This can be a completely empty argument list, like `{ () in ... }`.
|
|
if firstTokenInArgumentsList == endOfArgumentsScopeIndex {
|
|
return (argumentNames: [], inKeywordIndex: inKeywordIndex)
|
|
}
|
|
|
|
// Parse the comma-separated list of arguments
|
|
var currentArgIndex = firstTokenInArgumentsList
|
|
while tokens[currentArgIndex].isIdentifierOrKeyword {
|
|
argumentNames.append(tokens[currentArgIndex].string)
|
|
|
|
guard let nextComma = index(of: .delimiter(","), in: currentArgIndex ..< endOfArgumentsScopeIndex),
|
|
let nextArgLabel = index(of: .identifierOrKeyword, in: nextComma ..< endOfArgumentsScopeIndex)
|
|
else {
|
|
break
|
|
}
|
|
|
|
currentArgIndex = nextArgLabel
|
|
}
|
|
}
|
|
|
|
// Otherwise this is an anonymous closure
|
|
else {
|
|
return nil
|
|
}
|
|
|
|
return (argumentNames: argumentNames, inKeywordIndex: inKeywordIndex)
|
|
}
|
|
|
|
/// A fully parsed closure arguments list
|
|
struct ClosureArguments {
|
|
/// The range of the capture list `[...]` if present
|
|
let captureListRange: ClosedRange<Int>?
|
|
/// The index of any global actor attribute like `@MainActor`
|
|
let globalActorIndex: Int?
|
|
/// The range of the parameters (either bare identifiers or parenthesized list)
|
|
let parametersRange: ClosedRange<Int>?
|
|
/// The indices of individual argument identifiers
|
|
let argumentIndices: [Int]
|
|
/// The range of the return type `-> Type` if present
|
|
let returnTypeRange: ClosedRange<Int>?
|
|
/// The index of the `in` keyword
|
|
let inKeywordIndex: Int
|
|
}
|
|
|
|
/// Parses closure arguments from the `{` start of closure through to the `in` keyword.
|
|
/// Returns nil if the closure has no arguments or if parsing fails.
|
|
func parseClosureArguments(at closureStartIndex: Int) -> ClosureArguments? {
|
|
assert(tokens[closureStartIndex] == .startOfScope("{"))
|
|
|
|
var currentIndex = closureStartIndex
|
|
|
|
// Check for global actor like @MainActor (can appear before capture list)
|
|
var globalActorIndex: Int?
|
|
if let nextToken = index(of: .nonSpaceOrCommentOrLinebreak, after: currentIndex),
|
|
tokens[nextToken].isAttribute
|
|
{
|
|
globalActorIndex = nextToken
|
|
currentIndex = nextToken
|
|
}
|
|
|
|
// Parse optional capture list [weak self, unowned bar]
|
|
var captureListRange: ClosedRange<Int>?
|
|
if let firstToken = index(of: .nonSpaceOrCommentOrLinebreak, after: currentIndex),
|
|
tokens[firstToken] == .startOfScope("["),
|
|
let captureListEnd = endOfScope(at: firstToken)
|
|
{
|
|
captureListRange = firstToken ... captureListEnd
|
|
currentIndex = captureListEnd
|
|
}
|
|
|
|
// Check for global actor after capture list (if not found before)
|
|
if globalActorIndex == nil,
|
|
let nextToken = index(of: .nonSpaceOrCommentOrLinebreak, after: currentIndex),
|
|
tokens[nextToken].isAttribute
|
|
{
|
|
globalActorIndex = nextToken
|
|
currentIndex = nextToken
|
|
}
|
|
|
|
// Now look for arguments - either bare identifiers or parenthesized list
|
|
guard let firstParamToken = index(of: .nonSpaceOrCommentOrLinebreak, after: currentIndex) else {
|
|
return nil
|
|
}
|
|
|
|
var argumentIndices: [Int] = []
|
|
var parametersRange: ClosedRange<Int>?
|
|
var returnTypeRange: ClosedRange<Int>?
|
|
|
|
// Case 1: Parenthesized parameters like { (foo: Int, bar: String) in }
|
|
if tokens[firstParamToken] == .startOfScope("(") {
|
|
guard let paramsEnd = endOfScope(at: firstParamToken) else {
|
|
return nil
|
|
}
|
|
|
|
parametersRange = firstParamToken ... paramsEnd
|
|
|
|
// Parse arguments inside parens
|
|
var argIndex = firstParamToken + 1
|
|
while argIndex < paramsEnd {
|
|
if let nextNonSpace = index(of: .nonSpaceOrCommentOrLinebreak, in: argIndex ..< paramsEnd),
|
|
tokens[nextNonSpace].isIdentifierOrKeyword
|
|
{
|
|
argumentIndices.append(nextNonSpace)
|
|
|
|
// Skip to next comma or end of scope
|
|
if let nextComma = index(of: .delimiter(","), in: nextNonSpace ..< paramsEnd) {
|
|
argIndex = nextComma + 1
|
|
} else {
|
|
break
|
|
}
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
currentIndex = paramsEnd
|
|
|
|
// Skip past throws/rethrows/async keywords and return type
|
|
if let nextTokenIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: currentIndex) {
|
|
var idx = nextTokenIndex
|
|
// Skip throws/rethrows/async (including typed throws like throws(Foo))
|
|
while [.keyword("throws"), .keyword("rethrows"), .identifier("async")].contains(tokens[idx]) {
|
|
// Handle typed throws: throws(ErrorType)
|
|
if let parenStart = index(of: .nonSpaceOrCommentOrLinebreak, after: idx),
|
|
tokens[parenStart] == .startOfScope("("),
|
|
let parenEnd = endOfScope(at: parenStart),
|
|
let next = index(of: .nonSpaceOrCommentOrLinebreak, after: parenEnd)
|
|
{
|
|
idx = next
|
|
} else if let next = index(of: .nonSpaceOrCommentOrLinebreak, after: idx) {
|
|
idx = next
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
// Skip return type (-> Type)
|
|
if tokens[idx] == .operator("->", .infix),
|
|
let returnTypeStart = index(of: .nonSpaceOrCommentOrLinebreak, after: idx),
|
|
let returnType = parseType(at: returnTypeStart)
|
|
{
|
|
returnTypeRange = nextTokenIndex ... returnType.range.upperBound
|
|
currentIndex = returnType.range.upperBound
|
|
} else if idx != nextTokenIndex {
|
|
// Had throws/rethrows/async keywords - advance past them
|
|
currentIndex = index(of: .nonSpaceOrCommentOrLinebreak, before: idx) ?? currentIndex
|
|
}
|
|
}
|
|
}
|
|
// Case 2: Bare identifiers like { foo, bar in }
|
|
else if tokens[firstParamToken].isIdentifier {
|
|
let paramsStart = firstParamToken
|
|
var paramsEnd = firstParamToken
|
|
|
|
// Parse bare identifier list
|
|
var argIndex = firstParamToken
|
|
while argIndex < tokens.count {
|
|
if tokens[argIndex].isIdentifier {
|
|
argumentIndices.append(argIndex)
|
|
paramsEnd = argIndex
|
|
|
|
// Check what comes after this identifier
|
|
if let nextNonSpace = index(of: .nonSpaceOrCommentOrLinebreak, after: argIndex) {
|
|
if tokens[nextNonSpace] == .delimiter(",") {
|
|
// Continue to next parameter
|
|
argIndex = nextNonSpace + 1
|
|
continue
|
|
} else if tokens[nextNonSpace] == .keyword("in")
|
|
|| tokens[nextNonSpace] == .operator("->", .infix)
|
|
|| tokens[nextNonSpace] == .keyword("throws")
|
|
|| tokens[nextNonSpace] == .keyword("rethrows")
|
|
|| tokens[nextNonSpace] == .identifier("async")
|
|
{
|
|
// Found the end of parameters
|
|
break
|
|
} else {
|
|
// Unexpected token
|
|
return nil
|
|
}
|
|
} else {
|
|
break
|
|
}
|
|
} else if tokens[argIndex].isSpaceOrCommentOrLinebreak {
|
|
argIndex += 1
|
|
} else {
|
|
// Unexpected token
|
|
return nil
|
|
}
|
|
}
|
|
|
|
if !argumentIndices.isEmpty {
|
|
parametersRange = paramsStart ... paramsEnd
|
|
}
|
|
|
|
currentIndex = paramsEnd
|
|
|
|
// Skip past throws/rethrows/async keywords and return type
|
|
if let nextTokenIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: currentIndex) {
|
|
var idx = nextTokenIndex
|
|
// Skip throws/rethrows/async (including typed throws like throws(Foo))
|
|
while [.keyword("throws"), .keyword("rethrows"), .identifier("async")].contains(tokens[idx]) {
|
|
if let parenStart = index(of: .nonSpaceOrCommentOrLinebreak, after: idx),
|
|
tokens[parenStart] == .startOfScope("("),
|
|
let parenEnd = endOfScope(at: parenStart),
|
|
let next = index(of: .nonSpaceOrCommentOrLinebreak, after: parenEnd)
|
|
{
|
|
idx = next
|
|
} else if let next = index(of: .nonSpaceOrCommentOrLinebreak, after: idx) {
|
|
idx = next
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
// Skip return type (-> Type)
|
|
if tokens[idx] == .operator("->", .infix),
|
|
let returnTypeStart = index(of: .nonSpaceOrCommentOrLinebreak, after: idx),
|
|
let returnType = parseType(at: returnTypeStart)
|
|
{
|
|
returnTypeRange = nextTokenIndex ... returnType.range.upperBound
|
|
currentIndex = returnType.range.upperBound
|
|
} else if idx != nextTokenIndex {
|
|
currentIndex = index(of: .nonSpaceOrCommentOrLinebreak, before: idx) ?? currentIndex
|
|
}
|
|
}
|
|
}
|
|
|
|
// Must find 'in' keyword
|
|
guard let inKeywordIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: currentIndex),
|
|
tokens[inKeywordIndex] == .keyword("in")
|
|
else {
|
|
return nil
|
|
}
|
|
|
|
return ClosureArguments(
|
|
captureListRange: captureListRange,
|
|
globalActorIndex: globalActorIndex,
|
|
parametersRange: parametersRange,
|
|
argumentIndices: argumentIndices,
|
|
returnTypeRange: returnTypeRange,
|
|
inKeywordIndex: inKeywordIndex
|
|
)
|
|
}
|
|
|
|
/// Get the type of the declaration starting at the index of the declaration keyword
|
|
func declarationType(at index: Int) -> String? {
|
|
guard let token = token(at: index), token.isDeclarationTypeKeyword,
|
|
case let .keyword(keyword) = token
|
|
else {
|
|
return nil
|
|
}
|
|
if keyword == "class" {
|
|
var nextIndex = index
|
|
while let i = self.index(of: .nonSpaceOrCommentOrLinebreak, after: nextIndex) {
|
|
let nextToken = tokens[i]
|
|
if nextToken.isDeclarationTypeKeyword {
|
|
return nextToken.string
|
|
}
|
|
guard nextToken.isModifierKeyword else {
|
|
break
|
|
}
|
|
nextIndex = i
|
|
}
|
|
return keyword
|
|
}
|
|
return keyword
|
|
}
|
|
|
|
/// Gather declared name(s), starting at the index of the declaration keyword
|
|
func namesInDeclaration(at index: Int) -> Set<String>? {
|
|
guard case let .keyword(keyword)? = token(at: index) else {
|
|
return nil
|
|
}
|
|
switch keyword {
|
|
case "let", "var":
|
|
var index = index + 1
|
|
var names = Set<String>()
|
|
processDeclaredVariables(at: &index, names: &names)
|
|
return names
|
|
case "func", "class", "actor", "struct", "enum":
|
|
guard let name = next(.identifier, after: index) else {
|
|
return nil
|
|
}
|
|
return [name.string]
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
/// The type of scope that a declaration is contained within
|
|
enum DeclarationScope {
|
|
/// The declaration is a top-level global
|
|
case global
|
|
|
|
/// The declaration is a member of some type
|
|
case type
|
|
|
|
/// The declaration is within some local scope,
|
|
/// like a function body or closure.
|
|
case local
|
|
}
|
|
|
|
/// Returns the index of the start of the declaration scope that the given token index is contained by,
|
|
/// and the type (global, type, or local)
|
|
func declarationIndexAndScope(at i: Int) -> (index: Int?, scope: DeclarationScope) {
|
|
// Declarations which have `DeclarationScope.type`
|
|
let typeDeclarations = Set(["class", "actor", "struct", "enum", "protocol", "extension"])
|
|
|
|
// back track through tokens until we find a startOfScope("{") that isDeclarationTypeKeyword
|
|
// - we have to skip scopes that sit between this token and the its actual start of scope,
|
|
// so we have to keep track of the number of unpaired end scope tokens we have encountered
|
|
var unpairedEndScopeCount = 0
|
|
var currentIndex = i
|
|
var startOfScope: Int?
|
|
|
|
while startOfScope == nil, currentIndex > 0 {
|
|
currentIndex -= 1
|
|
|
|
if tokens[currentIndex] == .endOfScope("}") {
|
|
unpairedEndScopeCount += 1
|
|
} else if tokens[currentIndex] == .startOfScope("{") {
|
|
// If we find a closure or conditional statement that contains the index we're checking,
|
|
// we know the inner code is local.
|
|
if let endOfScope = endOfScope(at: currentIndex),
|
|
(currentIndex ... endOfScope).contains(i),
|
|
isStartOfClosureOrFunctionBody(at: currentIndex) || isConditionalStatement(at: currentIndex)
|
|
{
|
|
return (nil, .local)
|
|
}
|
|
|
|
if unpairedEndScopeCount == 0 {
|
|
startOfScope = currentIndex
|
|
} else {
|
|
unpairedEndScopeCount -= 1
|
|
}
|
|
}
|
|
}
|
|
|
|
// If this declaration isn't within any scope, it must be a global.
|
|
guard let startOfScopeIndex = startOfScope else {
|
|
return (nil, isConditionalStatement(at: i) ? .local : .global)
|
|
}
|
|
|
|
// Code within closures and conditionals is always local
|
|
if isStartOfClosureOrFunctionBody(at: startOfScopeIndex) || isConditionalStatement(at: startOfScopeIndex) {
|
|
return (nil, .local)
|
|
}
|
|
|
|
guard let declarationKeywordIndex = indexOfLastSignificantKeyword(
|
|
at: startOfScopeIndex, excluding: ["where"]
|
|
) else {
|
|
// Probably a contextual keyword like get, set, didSet, willSet, etc
|
|
return (nil, .local)
|
|
}
|
|
|
|
if typeDeclarations.contains(tokens[declarationKeywordIndex].string) {
|
|
return (declarationKeywordIndex, .type)
|
|
} else {
|
|
return (declarationKeywordIndex, .local)
|
|
}
|
|
}
|
|
|
|
/// Returns the declaration scope (global, type, or local) that the
|
|
/// given token index is contained by.
|
|
func declarationScope(at i: Int) -> DeclarationScope {
|
|
declarationIndexAndScope(at: i).scope
|
|
}
|
|
|
|
/// Indent level to use for wrapped lines at the specified position (based on statement type)
|
|
func linewrapIndent(at index: Int) -> String {
|
|
guard let commaIndex = self.index(of: .nonSpaceOrCommentOrLinebreak, before: index + 1, if: {
|
|
$0 == .delimiter(",")
|
|
}),
|
|
case let firstToken = startOfLine(at: commaIndex, excludingIndent: true),
|
|
let firstNonBrace = (firstToken ..< commaIndex).first(where: {
|
|
let token = self.tokens[$0]
|
|
return !token.isEndOfScope && !token.isSpaceOrComment
|
|
})
|
|
else {
|
|
if next(.nonSpaceOrCommentOrLinebreak, after: index) == .operator(".", .infix),
|
|
var prevIndex = self.index(of: .nonSpaceOrLinebreak, before: index)
|
|
{
|
|
if case .endOfScope = tokens[prevIndex] {
|
|
prevIndex = self.index(of: .startOfScope, before: index) ?? prevIndex
|
|
}
|
|
if case let lineStart = startOfLine(at: prevIndex, excludingIndent: true),
|
|
tokens[lineStart] == .operator(".", .infix),
|
|
self.index(of: .startOfScope, before: index) ?? -1 < lineStart
|
|
{
|
|
return currentIndentForLine(at: lineStart)
|
|
}
|
|
}
|
|
return options.indent
|
|
}
|
|
if case .endOfScope = tokens[firstToken],
|
|
next(.nonSpaceOrCommentOrLinebreak, after: firstNonBrace - 1) == .delimiter(",")
|
|
{
|
|
return ""
|
|
}
|
|
guard let keywordIndex = lastIndex(in: firstNonBrace ..< commaIndex, where: {
|
|
[.keyword("if"), .keyword("guard"), .keyword("while")].contains($0)
|
|
}) ?? lastIndex(in: firstNonBrace ..< commaIndex, where: {
|
|
[.keyword("let"), .keyword("var"), .keyword("case")].contains($0)
|
|
}), let nextTokenIndex = self.index(of: .nonSpace, after: keywordIndex) else {
|
|
return options.indent
|
|
}
|
|
return spaceEquivalentToTokens(from: firstToken, upTo: nextTokenIndex)
|
|
}
|
|
|
|
/// Returns end of last index of Void type declaration starting at specified index, or nil if not Void
|
|
func endOfVoidType(at index: Int) -> Int? {
|
|
switch tokens[index] {
|
|
case .identifier("Void"):
|
|
return index
|
|
case .identifier("Swift"):
|
|
guard let dotIndex = self.index(of: .nonSpaceOrLinebreak, after: index, if: {
|
|
$0 == .operator(".", .infix)
|
|
}), let voidIndex = self.index(of: .nonSpace, after: dotIndex, if: {
|
|
$0 == .identifier("Void")
|
|
}) else { return nil }
|
|
return voidIndex
|
|
case .startOfScope("("):
|
|
guard let nextIndex = self.index(of: .nonSpace, after: index) else {
|
|
return nil
|
|
}
|
|
switch tokens[nextIndex] {
|
|
case .endOfScope(")"):
|
|
return nextIndex
|
|
case .identifier("Void"):
|
|
guard let nextIndex = self.index(of: .nonSpace, after: nextIndex),
|
|
case .endOfScope(")") = tokens[nextIndex]
|
|
else {
|
|
return nil
|
|
}
|
|
return nextIndex
|
|
default:
|
|
return nil
|
|
}
|
|
default:
|
|
return nil
|
|
}
|
|
}
|
|
|
|
/// Range of tokens forming file header comment
|
|
func headerCommentTokenRange(includingDirectives directives: [String] = []) -> Range<Int>? {
|
|
guard !options.fragment else {
|
|
return nil
|
|
}
|
|
var start = 0
|
|
var lastHeaderTokenIndex = -1
|
|
if var startIndex = index(of: .nonSpaceOrLinebreak, after: -1) {
|
|
if tokens[startIndex] == .startOfScope("#!") {
|
|
guard let endIndex = index(of: .linebreak, after: startIndex) else {
|
|
return nil
|
|
}
|
|
startIndex = index(of: .nonSpaceOrLinebreak, after: endIndex) ?? endIndex
|
|
start = startIndex
|
|
lastHeaderTokenIndex = startIndex - 1
|
|
}
|
|
switch tokens[startIndex] {
|
|
case .startOfScope("//"):
|
|
if case let .commentBody(body)? = next(.nonSpace, after: startIndex) {
|
|
updateEnablement(at: startIndex)
|
|
if !isEnabled || (body.hasPrefix("/") && !body.hasPrefix("//")) ||
|
|
body.hasPrefix("swift-tools-version")
|
|
{
|
|
return nil
|
|
} else if let directive = body.commentDirective,
|
|
!directives.contains(directive),
|
|
directives != ["*"]
|
|
{
|
|
break
|
|
}
|
|
}
|
|
var lastIndex = startIndex
|
|
while let index = index(of: .linebreak, after: lastIndex) {
|
|
switch token(at: index + 1) ?? .space("") {
|
|
case .startOfScope("//"):
|
|
if case let .commentBody(body)? = next(.nonSpace, after: index + 1),
|
|
let directive = body.commentDirective,
|
|
!directives.contains(directive),
|
|
directives != ["*"]
|
|
{
|
|
break
|
|
}
|
|
lastIndex = index
|
|
continue
|
|
case .linebreak:
|
|
lastHeaderTokenIndex = index + 1
|
|
case .space where token(at: index + 2)?.isLinebreak == true:
|
|
lastHeaderTokenIndex = index + 2
|
|
default:
|
|
break
|
|
}
|
|
break
|
|
}
|
|
case .startOfScope("/*"):
|
|
if case let .commentBody(body)? = next(.nonSpace, after: startIndex) {
|
|
updateEnablement(at: startIndex)
|
|
if !isEnabled || (body.hasPrefix("*") && !body.hasPrefix("**")) {
|
|
return nil
|
|
} else if body.isCommentDirective {
|
|
break
|
|
}
|
|
}
|
|
while let endIndex = index(of: .endOfScope("*/"), after: startIndex) {
|
|
lastHeaderTokenIndex = endIndex
|
|
if let linebreakIndex = index(of: .linebreak, after: endIndex) {
|
|
lastHeaderTokenIndex = linebreakIndex
|
|
}
|
|
guard let nextIndex = index(of: .nonSpace, after: lastHeaderTokenIndex) else {
|
|
break
|
|
}
|
|
guard tokens[nextIndex] == .startOfScope("/*") else {
|
|
if let endIndex = index(of: .nonSpaceOrLinebreak, after: lastHeaderTokenIndex) {
|
|
lastHeaderTokenIndex = endIndex - 1
|
|
}
|
|
break
|
|
}
|
|
startIndex = nextIndex
|
|
}
|
|
case .endOfScope("*/"), .linebreak:
|
|
updateEnablement(at: startIndex)
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
return start ..< lastHeaderTokenIndex + 1
|
|
}
|
|
|
|
/// Finds the set of single-line comments in the given range matching the given closure
|
|
func singleLineComments(
|
|
in range: Range<Int>,
|
|
matching isMatch: (_ commentBody: String) -> Bool
|
|
) -> [ClosedRange<Int>] {
|
|
var matches = [ClosedRange<Int>]()
|
|
|
|
for commentStartIndex in range {
|
|
guard tokens[commentStartIndex] == .startOfScope("//"),
|
|
let commentBodyIndex = index(after: commentStartIndex, where: \.isCommentBody)
|
|
else { continue }
|
|
|
|
if isMatch(tokens[commentBodyIndex].string) {
|
|
matches.append(commentStartIndex ... commentBodyIndex)
|
|
}
|
|
}
|
|
|
|
return matches
|
|
}
|
|
|
|
/// Parses the range of the doc comment or regular comment immediately preceding the declaration
|
|
func parseDocCommentRange(forDeclarationAt keywordIndex: Int) -> ClosedRange<Int>? {
|
|
let startOfModifiers = startOfModifiers(at: keywordIndex, includingAttributes: true)
|
|
|
|
var parseIndex = startOfModifiers
|
|
var endOfComment: Int?
|
|
|
|
while let endOfPreviousLine = index(of: .linebreak, before: parseIndex),
|
|
let endOfPreviousLineContent = index(of: .nonSpace, before: endOfPreviousLine),
|
|
tokens[endOfPreviousLineContent].isComment,
|
|
let startOfScope = startOfScope(at: endOfPreviousLineContent)
|
|
{
|
|
parseIndex = startOfScope
|
|
|
|
if endOfComment == nil {
|
|
endOfComment = endOfPreviousLineContent
|
|
}
|
|
}
|
|
|
|
guard let endOfComment else { return nil }
|
|
return parseIndex ... endOfComment
|
|
}
|
|
|
|
/// Parses the prorocol composition typealias declaration starting at the given `typealias` keyword index.
|
|
/// Returns `nil` if the given index isn't a protocol composition typealias.
|
|
func parseProtocolCompositionTypealias(at typealiasIndex: Int)
|
|
-> (equalsIndex: Int, andTokenIndices: [Int], endIndex: Int)?
|
|
{
|
|
guard let equalsIndex = index(of: .operator("=", .infix), after: typealiasIndex),
|
|
let startOfType = index(of: .nonSpaceOrCommentOrLinebreak, after: equalsIndex),
|
|
let fullProtocolCompositionType = parseType(at: startOfType)
|
|
else { return nil }
|
|
|
|
var andTokenIndices: [Int] = []
|
|
var currentIndex = startOfType
|
|
|
|
while let nextType = parseType(at: currentIndex, excludeProtocolCompositions: true),
|
|
let nextAndToken = index(of: .nonSpaceOrCommentOrLinebreak, after: nextType.range.upperBound),
|
|
tokens[nextAndToken] == .operator("&", .infix),
|
|
let tokenAfterAndToken = index(of: .nonSpaceOrCommentOrLinebreak, after: nextAndToken)
|
|
{
|
|
andTokenIndices.append(nextAndToken)
|
|
currentIndex = tokenAfterAndToken
|
|
}
|
|
|
|
// If we didn't find any `&` tokens then this isn't a protocol composition typealias.
|
|
guard !andTokenIndices.isEmpty else { return nil }
|
|
|
|
return (equalsIndex, andTokenIndices, fullProtocolCompositionType.range.upperBound)
|
|
}
|
|
|
|
/// A fully parsed function declaration
|
|
struct FunctionDeclaration {
|
|
/// The index of the `func`, `subscript`, or `init` keyword
|
|
let keywordIndex: Int
|
|
/// The name of the function
|
|
let name: String?
|
|
/// The index of the function name's `identifier` token
|
|
let nameIndex: Int?
|
|
/// The range of of the generic parameters clause (`<...>`) if present
|
|
let genericParameterRange: ClosedRange<Int>?
|
|
/// The range of the function arguments (`(...)`)
|
|
let argumentsRange: ClosedRange<Int> // the range of (...)
|
|
/// The parsed function arguments within `argumentsRange`
|
|
let arguments: [FunctionArgument]
|
|
/// The range of effects keywords (`async`, `throws`, `rethrows`)
|
|
let effectsRange: ClosedRange<Int>?
|
|
/// The effects applied to the function in `effectsRange`
|
|
let effects: Set<String>
|
|
/// The index of the `->` operator if present
|
|
let returnOperatorIndex: Int?
|
|
/// The parsed return type if present
|
|
let returnType: TypeName?
|
|
/// The range of the `where` clause if present
|
|
let whereClauseRange: ClosedRange<Int>?
|
|
/// The range of the function body (`{ ... }`) if present.
|
|
/// A protocol method requirement, or a function with a `@_silgen` attribute, doesn't have a body.
|
|
let bodyRange: ClosedRange<Int>?
|
|
|
|
/// The full range of this declaration
|
|
var range: ClosedRange<Int> {
|
|
let endIndex = bodyRange?.upperBound
|
|
?? whereClauseRange?.upperBound
|
|
?? returnType?.range.upperBound
|
|
?? effectsRange?.upperBound
|
|
?? argumentsRange.upperBound
|
|
|
|
return keywordIndex ... endIndex
|
|
}
|
|
}
|
|
|
|
/// A function argument like `with foo: Foo`.
|
|
struct FunctionArgument: Equatable {
|
|
/// The external label of this argument. `nil` if omitted with an `_`.
|
|
let externalLabel: String?
|
|
|
|
/// The internal label of this argument. `nil` if omitted with an `_`.
|
|
let internalLabel: String?
|
|
|
|
/// The index of the external argument label. If nil, the argument
|
|
/// uses the same label both internally and externally.
|
|
let externalLabelIndex: Int?
|
|
|
|
/// The index of the internal argument name. Always present in the grammar.
|
|
let internalLabelIndex: Int
|
|
|
|
/// The type of the argument
|
|
var type: TypeName
|
|
|
|
/// Any attributes present before the argument, like `@ViewBuilder content: Content`
|
|
var attributes: [String]
|
|
}
|
|
|
|
/// Parses the function or function-like declaration (`func`, `subscript`, `init`) at the given keyword index
|
|
func parseFunctionDeclaration(keywordIndex: Int) -> FunctionDeclaration? {
|
|
assert(["func", "subscript", "init"].contains(tokens[keywordIndex].string))
|
|
var currentIndex = keywordIndex
|
|
|
|
var nameIndex: Int?
|
|
var name: String?
|
|
|
|
// Only function declarations (not subscripts / inits) have names
|
|
if tokens[keywordIndex] == .keyword("func") {
|
|
// The `func` / `subscript` keyword is always followed by the method name
|
|
guard let funcNameIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: keywordIndex) else {
|
|
return nil
|
|
}
|
|
|
|
nameIndex = funcNameIndex
|
|
name = tokens[funcNameIndex].string
|
|
currentIndex = funcNameIndex
|
|
}
|
|
|
|
// If this is a failable initializer (`init?`), skip over the ? token
|
|
if tokens[keywordIndex] == .keyword("init"),
|
|
let nextToken = index(of: .nonSpaceOrCommentOrLinebreak, after: keywordIndex),
|
|
tokens[nextToken] == .operator("?", .postfix)
|
|
{
|
|
currentIndex = nextToken
|
|
}
|
|
|
|
// Parse the optional generic parameters in `<...>`
|
|
var genericParameterRange: ClosedRange<Int>?
|
|
if let startOfGenericParameters = index(of: .nonSpaceOrCommentOrLinebreak, after: currentIndex),
|
|
tokens[startOfGenericParameters] == .startOfScope("<"),
|
|
let endOfGenericParameters = endOfScope(at: startOfGenericParameters)
|
|
{
|
|
genericParameterRange = startOfGenericParameters ... endOfGenericParameters
|
|
currentIndex = endOfGenericParameters
|
|
}
|
|
|
|
// All functions have an arguments list, following the optional generic parameters
|
|
guard let startOfArguments = index(of: .nonSpaceOrCommentOrLinebreak, after: currentIndex),
|
|
tokens[startOfArguments] == .startOfScope("("),
|
|
let endOfArguments = endOfScope(at: startOfArguments)
|
|
else { return nil }
|
|
|
|
let argumentsRange = startOfArguments ... endOfArguments
|
|
currentIndex = endOfArguments
|
|
|
|
let effects: Set<String>
|
|
let effectsRange: ClosedRange<Int>?
|
|
|
|
if let parsedEffects = parseFunctionDeclarationEffectsClause(at: currentIndex) {
|
|
effects = parsedEffects.effects
|
|
effectsRange = parsedEffects.range
|
|
currentIndex = parsedEffects.range.upperBound + 1
|
|
} else {
|
|
effects = []
|
|
effectsRange = nil
|
|
}
|
|
|
|
// Parse the optional return type
|
|
let returnOperatorIndex: Int?
|
|
let returnType: TypeName?
|
|
|
|
if let parsedReturnType = parseFunctionDeclarationReturnClause(at: currentIndex) {
|
|
returnOperatorIndex = parsedReturnType.returnOperatorIndex
|
|
returnType = parsedReturnType.returnType
|
|
currentIndex = parsedReturnType.returnType.range.upperBound
|
|
} else {
|
|
returnOperatorIndex = nil
|
|
returnType = nil
|
|
}
|
|
|
|
// Parse the optional where clause
|
|
var whereClauseRange: ClosedRange<Int>?
|
|
if let whereKeyword = index(of: .nonSpaceOrCommentOrLinebreak, after: currentIndex),
|
|
tokens[whereKeyword] == .keyword("where")
|
|
{
|
|
let parsedWhereClauseRange = parseGenericTypes(from: whereKeyword).range
|
|
whereClauseRange = parsedWhereClauseRange
|
|
currentIndex = parsedWhereClauseRange.upperBound
|
|
}
|
|
|
|
// Parse the optional body.
|
|
// If this is a protocol declaration, there will be no body.
|
|
var bodyRange: ClosedRange<Int>?
|
|
if let bodyOpenBrace = index(of: .nonSpaceOrCommentOrLinebreak, after: currentIndex),
|
|
tokens[bodyOpenBrace] == .startOfScope("{"),
|
|
let bodyClosingBrace = endOfScope(at: bodyOpenBrace)
|
|
{
|
|
bodyRange = bodyOpenBrace ... bodyClosingBrace
|
|
}
|
|
|
|
return FunctionDeclaration(
|
|
keywordIndex: keywordIndex,
|
|
name: name,
|
|
nameIndex: nameIndex,
|
|
genericParameterRange: genericParameterRange,
|
|
argumentsRange: argumentsRange,
|
|
arguments: parseFunctionDeclarationArguments(startOfScope: argumentsRange.lowerBound),
|
|
effectsRange: effectsRange,
|
|
effects: effects,
|
|
returnOperatorIndex: returnOperatorIndex,
|
|
returnType: returnType,
|
|
whereClauseRange: whereClauseRange,
|
|
bodyRange: bodyRange
|
|
)
|
|
}
|
|
|
|
/// Parses the arguments of the function with its `(` start of scope token at the given index.
|
|
func parseFunctionDeclarationArguments(startOfScope: Int) -> [FunctionArgument] {
|
|
assert(tokens[startOfScope] == .startOfScope("("))
|
|
guard let endOfScope = endOfScope(at: startOfScope) else { return [] }
|
|
|
|
var arguments: [FunctionArgument] = []
|
|
|
|
var currentIndex = startOfScope
|
|
while let nextArgumentColon = index(of: .delimiter(":"), in: (currentIndex + 1) ..< endOfScope) {
|
|
let colonIndex = nextArgumentColon
|
|
currentIndex = nextArgumentColon
|
|
|
|
// If there is only one label, the param has the same internal and external label.
|
|
// If there are two labels, the first one is the external label.
|
|
// We need to exclude attributes from being treated as labels
|
|
guard let internalLabelIndex = index(of: .nonSpaceOrComment, before: colonIndex),
|
|
tokens[internalLabelIndex].isIdentifier || tokens[internalLabelIndex].string == "_",
|
|
!tokens[internalLabelIndex].isAttribute
|
|
else { continue }
|
|
|
|
var externalLabelIndex = internalLabelIndex
|
|
var hasExplicitExternalLabel = false
|
|
|
|
if let possibleExternalLabelIndex = index(of: .nonSpaceOrComment, before: internalLabelIndex),
|
|
tokens[possibleExternalLabelIndex].isIdentifier || tokens[possibleExternalLabelIndex].string == "_",
|
|
!tokens[possibleExternalLabelIndex].isAttribute
|
|
{
|
|
externalLabelIndex = possibleExternalLabelIndex
|
|
hasExplicitExternalLabel = true
|
|
}
|
|
|
|
// Collect any attributes before the external label, like `@ViewBuilder`.
|
|
var attributes = [String]()
|
|
_ = modifiersForDeclaration(at: externalLabelIndex, contains: { _, modifier in
|
|
if modifier.isAttribute {
|
|
attributes.append(modifier)
|
|
}
|
|
return false
|
|
})
|
|
|
|
guard let startOfType = index(of: .nonSpaceOrComment, after: colonIndex),
|
|
let type = parseType(at: startOfType)
|
|
else { continue }
|
|
|
|
let identifierString: (Token) -> String? = { token in
|
|
if token.string == "_" {
|
|
return nil
|
|
} else {
|
|
return token.unescaped()
|
|
}
|
|
}
|
|
|
|
arguments.append(FunctionArgument(
|
|
externalLabel: identifierString(tokens[externalLabelIndex]),
|
|
internalLabel: identifierString(tokens[internalLabelIndex]),
|
|
externalLabelIndex: hasExplicitExternalLabel ? externalLabelIndex : nil,
|
|
internalLabelIndex: internalLabelIndex,
|
|
type: type,
|
|
attributes: attributes
|
|
))
|
|
}
|
|
|
|
return arguments
|
|
}
|
|
|
|
func parseFunctionDeclarationEffectsClause(at startIndex: Int) -> (effects: Set<String>, range: ClosedRange<Int>)? {
|
|
// Parse optional `async`, `throws`, `rethrows`, and typed throws `throws(...)` effects.
|
|
var effects = Set<String>()
|
|
var effectsRange: ClosedRange<Int>?
|
|
|
|
var currentIndex = startIndex
|
|
let firstIndexAfterArguments = index(of: .nonSpaceOrCommentOrLinebreak, after: currentIndex)
|
|
while let effectIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: currentIndex),
|
|
let effect = token(at: effectIndex)?.string,
|
|
["async", "throws", "rethrows"].contains(effect),
|
|
let firstIndexAfterArguments
|
|
{
|
|
// `throws` can optionally be typed throws with a `(Type)` component
|
|
if effect == "throws",
|
|
let startOfTypedThrows = index(of: .nonSpaceOrCommentOrLinebreak, after: effectIndex),
|
|
tokens[startOfTypedThrows] == .startOfScope("("),
|
|
let endOfTypedThrows = endOfScope(at: startOfTypedThrows)
|
|
{
|
|
effects.insert(tokens[effectIndex ... endOfTypedThrows].string)
|
|
effectsRange = firstIndexAfterArguments ... endOfTypedThrows
|
|
currentIndex = endOfTypedThrows
|
|
continue
|
|
}
|
|
|
|
// If an `async` keyword is immediately followed by `let` on the same line, this is probably an `async let` property.
|
|
// It's possible an `async` keyword to be followed by a `let` keyword without being an `async let` property
|
|
// (e.g. if the attached function doesn't have a body), so this seems fundamentally ambiguous in the grammar.
|
|
if effect == "async",
|
|
let nextToken = index(of: .nonSpaceOrComment, after: effectIndex),
|
|
tokens[nextToken] == .keyword("let")
|
|
{
|
|
break
|
|
}
|
|
|
|
effects.insert(effect)
|
|
effectsRange = firstIndexAfterArguments ... effectIndex
|
|
currentIndex = effectIndex
|
|
}
|
|
|
|
if let effectsRange {
|
|
return (effects: effects, range: effectsRange)
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func parseFunctionDeclarationReturnClause(at startIndex: Int) -> (returnOperatorIndex: Int, returnType: TypeName)? {
|
|
guard let returnIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: startIndex),
|
|
tokens[returnIndex] == .operator("->", .infix),
|
|
let indexAfterReturnOperator = index(of: .nonSpaceOrCommentOrLinebreak, after: returnIndex),
|
|
let parsedReturnType = parseType(at: indexAfterReturnOperator) else { return nil }
|
|
|
|
return (returnOperatorIndex: returnIndex, returnType: parsedReturnType)
|
|
}
|
|
|
|
struct FunctionCallArgument {
|
|
/// The label of the argument. `nil` if unlabeled.
|
|
let label: String?
|
|
/// The index of the optional label
|
|
let labelIndex: Int?
|
|
/// The value of the argument
|
|
let value: String
|
|
/// The index of the value
|
|
let valueRange: ClosedRange<Int>
|
|
}
|
|
|
|
/// Parses the parameter labels of the function call with its `(` start of scope
|
|
/// token at the given index.
|
|
func parseFunctionCallArguments(startOfScope: Int, preserveWhitespace: Bool = false) -> [FunctionCallArgument] {
|
|
assert(tokens[startOfScope] == .startOfScope("("))
|
|
guard let endOfScope = endOfScope(at: startOfScope),
|
|
index(of: .nonSpaceOrCommentOrLinebreak, after: startOfScope) != endOfScope
|
|
else { return [] }
|
|
|
|
var argumentLabels: [FunctionCallArgument] = []
|
|
|
|
var currentIndex = startOfScope
|
|
while currentIndex < endOfScope {
|
|
let endOfPreviousArgument = currentIndex
|
|
let endOfCurrentArgument = index(of: .delimiter(","), in: endOfPreviousArgument + 1 ..< endOfScope) ?? endOfScope
|
|
|
|
// If we find a trailing comma, then there's nothing else to parse
|
|
if index(of: .nonSpaceOrCommentOrLinebreak, after: endOfPreviousArgument) == endOfScope {
|
|
return argumentLabels
|
|
}
|
|
|
|
if let colonIndex = index(of: .delimiter(":"), in: (endOfPreviousArgument + 1) ..< endOfCurrentArgument),
|
|
let argumentLabelIndex = index(of: .nonSpaceOrCommentOrLinebreak, before: colonIndex),
|
|
tokens[argumentLabelIndex].isIdentifier
|
|
{
|
|
// Conditionally trim whitespace and newlines from the value range
|
|
var valueStart = colonIndex + 1
|
|
var valueEnd = endOfCurrentArgument - 1
|
|
|
|
if !preserveWhitespace {
|
|
while valueStart <= valueEnd, tokens[valueStart].isSpaceOrLinebreak {
|
|
valueStart += 1
|
|
}
|
|
while valueEnd >= valueStart, tokens[valueEnd].isSpaceOrLinebreak {
|
|
valueEnd -= 1
|
|
}
|
|
}
|
|
|
|
// Ensure we have a valid range
|
|
guard valueStart <= valueEnd else {
|
|
currentIndex = endOfCurrentArgument
|
|
continue
|
|
}
|
|
|
|
let valueRange = valueStart ... valueEnd
|
|
argumentLabels.append(FunctionCallArgument(
|
|
label: tokens[argumentLabelIndex].string,
|
|
labelIndex: argumentLabelIndex,
|
|
value: tokens[valueRange].string,
|
|
valueRange: valueRange
|
|
))
|
|
} else {
|
|
// Conditionally trim whitespace and newlines from the value range
|
|
var valueStart = endOfPreviousArgument + 1
|
|
var valueEnd = endOfCurrentArgument - 1
|
|
|
|
if !preserveWhitespace {
|
|
while valueStart <= valueEnd, tokens[valueStart].isSpaceOrLinebreak {
|
|
valueStart += 1
|
|
}
|
|
while valueEnd >= valueStart, tokens[valueEnd].isSpaceOrLinebreak {
|
|
valueEnd -= 1
|
|
}
|
|
}
|
|
|
|
// Ensure we have a valid range
|
|
guard valueStart <= valueEnd else {
|
|
currentIndex = endOfCurrentArgument
|
|
continue
|
|
}
|
|
|
|
let valueRange = valueStart ... valueEnd
|
|
argumentLabels.append(FunctionCallArgument(
|
|
label: nil,
|
|
labelIndex: nil,
|
|
value: tokens[valueRange].string,
|
|
valueRange: valueRange
|
|
))
|
|
}
|
|
|
|
if endOfCurrentArgument >= endOfScope {
|
|
break
|
|
} else {
|
|
currentIndex = endOfCurrentArgument
|
|
}
|
|
}
|
|
|
|
return argumentLabels
|
|
}
|
|
|
|
/// Parses the parameter labels of the tuple type or value with its `(` start of scope
|
|
/// token at the given index.
|
|
func parseTupleArguments(startOfScope: Int) -> [FunctionCallArgument] {
|
|
parseFunctionCallArguments(startOfScope: startOfScope)
|
|
}
|
|
|
|
/// Parses the list of conformances on this type, starting at
|
|
/// the index of the type keyword (`struct`, `class`, `extension`, etc).
|
|
func parseConformancesOfType(atKeywordIndex keywordIndex: Int) -> [(conformance: TypeName, index: Int)] {
|
|
assert(Token.swiftTypeKeywords.contains(tokens[keywordIndex].string))
|
|
|
|
guard let startOfType = index(of: .nonSpaceOrCommentOrLinebreak, after: keywordIndex),
|
|
let typeName = parseType(at: startOfType),
|
|
let indexAfterType = index(of: .nonSpaceOrCommentOrLinebreak, after: typeName.range.upperBound),
|
|
tokens[indexAfterType] == .delimiter(":"),
|
|
let firstConformanceIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: indexAfterType)
|
|
else { return [] }
|
|
|
|
var conformances = [(conformance: TypeName, index: Int)]()
|
|
var nextConformanceIndex = firstConformanceIndex
|
|
|
|
while let type = parseType(at: nextConformanceIndex) {
|
|
conformances.append((conformance: type, index: nextConformanceIndex))
|
|
|
|
if let nextTokenIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: type.range.upperBound) {
|
|
nextConformanceIndex = nextTokenIndex
|
|
|
|
// Skip over any comma tokens that separate conformances.
|
|
// If we find something other than a comma, like a `where` or `{`,
|
|
// then we reached the end of the conformance list.
|
|
guard tokens[nextTokenIndex] == .delimiter(","),
|
|
let followingConformanceIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: nextTokenIndex)
|
|
else {
|
|
return conformances
|
|
}
|
|
|
|
nextConformanceIndex = followingConformanceIndex
|
|
}
|
|
}
|
|
|
|
return conformances
|
|
}
|
|
|
|
/// Removes the protocol conformance at the given index.
|
|
/// e.g. can remove `Foo` from `Type: Foo, Bar {` (becomes `Type: Bar {`).
|
|
func removeConformance(at conformanceIndex: Int) {
|
|
guard let previousToken = index(of: .nonSpaceOrCommentOrLinebreak, before: conformanceIndex),
|
|
let nextToken = index(of: .nonSpaceOrCommentOrLinebreak, after: conformanceIndex)
|
|
else { return }
|
|
|
|
// The first conformance will be preceded by a colon.
|
|
// Every conformance but the last one will be followed by a comma.
|
|
// - for example: `Type: Foo, Bar, Baaz {`
|
|
let isFirstConformance = tokens[previousToken] == .delimiter(":")
|
|
let isLastConformance = tokens[nextToken] != .delimiter(",")
|
|
let isOnlyConformance = isFirstConformance && isLastConformance
|
|
|
|
if isLastConformance || isOnlyConformance {
|
|
removeTokens(in: previousToken ... conformanceIndex)
|
|
} else {
|
|
// When changing `Foo, Bar` to just `Bar`, also remove the space between them
|
|
if token(at: nextToken + 1)?.isSpace == true {
|
|
removeTokens(in: conformanceIndex ... (nextToken + 1))
|
|
} else {
|
|
removeTokens(in: conformanceIndex ... (nextToken + 1))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The explicit `Visibility` of the `Declaration` with its keyword at the given index
|
|
func declarationVisibility(keywordIndex: Int) -> Visibility? {
|
|
// Search for a visibility keyword in the tokens before the primary keyword,
|
|
// making sure we exclude groups like private(set).
|
|
var searchIndex = startOfModifiers(at: keywordIndex, includingAttributes: false)
|
|
while searchIndex < keywordIndex {
|
|
if let visibility = Visibility(rawValue: tokens[searchIndex].string),
|
|
next(.nonSpaceOrComment, after: searchIndex) != .startOfScope("(")
|
|
{
|
|
return visibility
|
|
}
|
|
|
|
searchIndex += 1
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
/// Adds the given visibility keyword to the given declaration,
|
|
/// replacing any existing visibility keyword.
|
|
func addDeclarationVisibility(_ visibilityKeyword: Visibility, declarationKeywordIndex: Int) {
|
|
var declarationKeywordIndex = declarationKeywordIndex
|
|
|
|
if let existingVisibility = declarationVisibility(keywordIndex: declarationKeywordIndex),
|
|
let visibilityKeywordIndex = indexOfModifier(existingVisibility.rawValue, forDeclarationAt: declarationKeywordIndex)
|
|
{
|
|
removeToken(at: visibilityKeywordIndex)
|
|
declarationKeywordIndex -= 1
|
|
|
|
while token(at: visibilityKeywordIndex)?.isSpace == true {
|
|
removeToken(at: visibilityKeywordIndex)
|
|
declarationKeywordIndex -= 1
|
|
}
|
|
}
|
|
|
|
insert(
|
|
[.keyword(visibilityKeyword.rawValue), .space(" ")],
|
|
at: startOfModifiers(at: declarationKeywordIndex, includingAttributes: false)
|
|
)
|
|
}
|
|
|
|
/// Removes the given visibility keyword from the given declaration
|
|
func removeDeclarationVisibility(_ visibilityKeyword: Visibility, declarationKeywordIndex: Int) {
|
|
guard let visibilityKeywordIndex = indexOfModifier(visibilityKeyword.rawValue, forDeclarationAt: declarationKeywordIndex) else { return }
|
|
|
|
removeToken(at: visibilityKeywordIndex)
|
|
|
|
while token(at: visibilityKeywordIndex)?.isSpace == true {
|
|
removeToken(at: visibilityKeywordIndex)
|
|
}
|
|
}
|
|
|
|
/// The name of the declaration defined at the given index
|
|
func declarationName(keywordIndex: Int) -> String? {
|
|
// Conditional compilation blocks don't have a "name"
|
|
guard tokens[keywordIndex].string != "#if" else { return nil }
|
|
|
|
guard let nameIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: keywordIndex) else {
|
|
return nil
|
|
}
|
|
|
|
// An extension can refer to complicated types like `Foo.Bar`, `[Foo]`, `Collection<Foo>`, etc.
|
|
// Every other declaration type just uses a simple identifier.
|
|
if tokens[keywordIndex].string == "extension" {
|
|
return parseType(at: nameIndex)?.string
|
|
} else {
|
|
return tokens[nameIndex].string
|
|
}
|
|
}
|
|
|
|
/// Represents a condition in a guard or if statement
|
|
enum ConditionalStatementElement {
|
|
/// A boolean expression like `foo == bar`
|
|
case booleanExpression(range: ClosedRange<Int>)
|
|
/// An optional binding / unwrap condition like `let foo` or `let foo = foo`
|
|
case optionalBinding(range: ClosedRange<Int>, property: PropertyDeclaration)
|
|
/// A pattern matching condition like `case .foo(let bar) = baaz`
|
|
case patternMatching(range: ClosedRange<Int>)
|
|
/// An availability condition like `#available(iOS 26.0, *)` or `#unavailable(iOS 26.0)`
|
|
case availabilityCondition(range: ClosedRange<Int>)
|
|
|
|
var range: ClosedRange<Int> {
|
|
switch self {
|
|
case let .booleanExpression(range), let .optionalBinding(range, _),
|
|
let .patternMatching(range), let .availabilityCondition(range):
|
|
return range
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Parse conditions in a guard or if statement
|
|
func parseConditionalStatement(at guardOrIfIndex: Int) -> [ConditionalStatementElement] {
|
|
assert(tokens[guardOrIfIndex] == .keyword("guard") || tokens[guardOrIfIndex] == .keyword("if"))
|
|
|
|
// Find the else keyword (or opening brace for if)
|
|
var endIndex: Int?
|
|
|
|
if let braceIndex = index(of: .startOfScope("{"), after: guardOrIfIndex) {
|
|
if tokens[guardOrIfIndex] == .keyword("if") {
|
|
// For if statements without else
|
|
endIndex = braceIndex
|
|
} else if let prevTokenIndex = index(of: .nonSpaceOrCommentOrLinebreak, before: braceIndex),
|
|
tokens[prevTokenIndex] == .keyword("else")
|
|
{
|
|
// For guard statements with else
|
|
endIndex = prevTokenIndex
|
|
}
|
|
}
|
|
|
|
guard let endIndex else { return [] }
|
|
|
|
var conditions: [ConditionalStatementElement] = []
|
|
var currentPos = guardOrIfIndex + 1
|
|
|
|
while currentPos < endIndex {
|
|
// Skip whitespace and comments
|
|
guard let conditionStart = index(of: .nonSpaceOrCommentOrLinebreak, after: currentPos - 1),
|
|
conditionStart < endIndex
|
|
else {
|
|
break
|
|
}
|
|
|
|
let conditionEnd: Int
|
|
if let commaIndex = index(of: .delimiter(","), after: conditionStart),
|
|
commaIndex < endIndex
|
|
{
|
|
conditionEnd = index(of: .nonSpaceOrCommentOrLinebreak, before: commaIndex) ?? conditionStart
|
|
} else {
|
|
conditionEnd = index(of: .nonSpaceOrCommentOrLinebreak, before: endIndex) ?? conditionStart
|
|
}
|
|
|
|
let element: ConditionalStatementElement
|
|
if tokens[conditionStart] == .keyword("case") {
|
|
element = .patternMatching(range: conditionStart ... conditionEnd)
|
|
} else if tokens[conditionStart] == .keyword("let") || tokens[conditionStart] == .keyword("var") {
|
|
guard let property = parsePropertyDeclaration(atIntroducerIndex: conditionStart) else {
|
|
return []
|
|
}
|
|
|
|
element = .optionalBinding(range: conditionStart ... conditionEnd, property: property)
|
|
} else if tokens[conditionStart] == .keyword("#available") || tokens[conditionStart] == .keyword("#unavailable") {
|
|
element = .availabilityCondition(range: conditionStart ... conditionEnd)
|
|
} else {
|
|
element = .booleanExpression(range: conditionStart ... conditionEnd)
|
|
}
|
|
|
|
conditions.append(element)
|
|
|
|
// Find next condition (after comma)
|
|
if let commaIndex = index(of: .delimiter(","), after: conditionEnd),
|
|
commaIndex < endIndex
|
|
{
|
|
currentPos = commaIndex + 1
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
return conditions
|
|
}
|
|
|
|
/// Parses the function identifier before the `(` start of scope token.
|
|
/// Handles `foo(`, `foo?(`, and `foo!(`.
|
|
func parseFunctionIdentifier(beforeStartOfScope startOfScope: Int) -> Int? {
|
|
assert(tokens[startOfScope] == .startOfScope("("))
|
|
guard let previousToken = index(of: .nonSpaceOrCommentOrLinebreak, before: startOfScope) else { return nil }
|
|
|
|
// `foo()`, `@foo()`, or `#foo()`
|
|
// Exclude keywords to avoid confusing `return (...)`, `as? (...)`, `{ _ in (...) }`, etc.
|
|
let isFunctionIdentifier = { (token: Token) in
|
|
token.isIdentifier || token.isAttribute || token.isMacro || token.string == "init"
|
|
}
|
|
|
|
if isFunctionIdentifier(tokens[previousToken]) {
|
|
return previousToken
|
|
}
|
|
|
|
if [.operator("?", .postfix), .operator("!", .postfix)].contains(tokens[previousToken]),
|
|
let tokenBeforeOperator = index(of: .nonSpaceOrCommentOrLinebreak, before: previousToken),
|
|
isFunctionIdentifier(tokens[tokenBeforeOperator])
|
|
{
|
|
return tokenBeforeOperator
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
extension _FormatRules {
|
|
/// Swiftlint semantic modifier groups
|
|
static let semanticModifierGroups = ["acl", "setteracl", "mutators", "typemethods", "owned"]
|
|
|
|
/// All modifiers
|
|
static let allModifiers = Set(defaultModifierOrder.flatMap { $0 })
|
|
|
|
/// ACL modifiers
|
|
static let aclModifiers = ["private", "fileprivate", "internal", "package", "public", "open"]
|
|
|
|
/// ACL setter modifiers
|
|
static let aclSetterModifiers = aclModifiers.map { "\($0)(set)" }
|
|
|
|
/// Mutating modifiers
|
|
static let mutatingModifiers = ["borrowing", "consuming", "mutating", "nonmutating"]
|
|
|
|
/// Ownership modifiers
|
|
static let ownershipModifiers = ["weak", "unowned", "unowned(safe)", "unowned(unsafe)"]
|
|
|
|
/// Modifier mapping (designed to match SwiftLint)
|
|
static func mapModifiers(_ input: String) -> [String]? {
|
|
switch input.lowercased() {
|
|
case "acl":
|
|
return aclModifiers
|
|
case "setteracl":
|
|
return aclSetterModifiers
|
|
case "mutators":
|
|
return mutatingModifiers
|
|
case "typemethods":
|
|
return [] // Not clear what this is for - legacy?
|
|
case "owned":
|
|
return ownershipModifiers
|
|
case let input:
|
|
if allModifiers.contains(input) {
|
|
return [input]
|
|
}
|
|
guard let index = input.firstIndex(of: "(") else {
|
|
return nil
|
|
}
|
|
let input = String(input[..<index])
|
|
return allModifiers.contains(input) ? [input] : nil
|
|
}
|
|
}
|
|
|
|
/// Swift modifier keywords, in default order
|
|
static let defaultModifierOrder = [
|
|
["override"],
|
|
aclModifiers,
|
|
aclSetterModifiers,
|
|
["final", "dynamic"],
|
|
["optional", "required"],
|
|
["convenience"],
|
|
["indirect"],
|
|
["isolated", "nonisolated", "nonisolated(unsafe)"],
|
|
["lazy"],
|
|
ownershipModifiers,
|
|
["static", "class"],
|
|
mutatingModifiers,
|
|
["prefix", "infix", "postfix"],
|
|
["async"],
|
|
]
|
|
|
|
/// Global swift functions
|
|
static let globalSwiftFunctions = [
|
|
"min", "max", "abs", "print", "stride", "zip",
|
|
]
|
|
}
|
|
|
|
extension Token {
|
|
/// Whether or not this token "defines" the specific type of declaration
|
|
/// - A valid declaration will usually include exactly one of these keywords in its outermost scope.
|
|
/// - Notable exceptions are `class func` and symbol imports (like `import class Module.Type`)
|
|
/// which will include two of these keywords.
|
|
var isDeclarationTypeKeyword: Bool {
|
|
isDeclarationTypeKeyword(excluding: [])
|
|
}
|
|
|
|
/// All of the keywords defining top-level entity
|
|
/// https://docs.swift.org/swift-book/ReferenceManual/Declarations.html#grammar_declaration
|
|
static var swiftTypeKeywords: Set<String> {
|
|
Set(["struct", "class", "actor", "protocol", "enum", "extension"])
|
|
}
|
|
|
|
/// All of the keywords that map to individual Declaration grammars
|
|
/// https://docs.swift.org/swift-book/ReferenceManual/Declarations.html#grammar_declaration
|
|
static var declarationTypeKeywords: Set<String> {
|
|
swiftTypeKeywords.union([
|
|
"import", "let", "var", "typealias", "func", "enum", "case",
|
|
"struct", "class", "actor", "protocol", "init", "deinit",
|
|
"extension", "subscript", "operator", "precedencegroup",
|
|
"associatedtype", "macro",
|
|
])
|
|
}
|
|
|
|
/// Whether or not this token "defines" the specific type of declaration
|
|
/// - A valid declaration will usually include exactly one of these keywords in its outermost scope.
|
|
/// - Notable exceptions are `class func` and symbol imports (like `import class Module.Type`)
|
|
/// which will include two of these keywords.
|
|
func isDeclarationTypeKeyword(excluding keywordsToExclude: [String]) -> Bool {
|
|
guard case let .keyword(keyword) = self else {
|
|
return false
|
|
}
|
|
|
|
return Self.declarationTypeKeywords
|
|
.subtracting(keywordsToExclude)
|
|
.contains(keyword)
|
|
}
|
|
|
|
/// Whether or not this token "defines" the specific type of declaration
|
|
/// - A valid declaration will usually include exactly one of these keywords in its outermost scope.
|
|
/// - Notable exceptions are `class func` and symbol imports (like `import class Module.Type`)
|
|
/// which will include two of these keywords.
|
|
func isDeclarationTypeKeyword(including keywordsToInclude: [String]) -> Bool {
|
|
guard case let .keyword(keyword) = self else {
|
|
return false
|
|
}
|
|
|
|
return Self.declarationTypeKeywords
|
|
.intersection(keywordsToInclude)
|
|
.contains(keyword)
|
|
}
|
|
|
|
/// Whether or not this token represents a potential modifier keyword.
|
|
/// This doesn't necessarily mean that the keyword is a modifier: some modifiers
|
|
/// like `class` and `async` are contextual.
|
|
/// In rule implementations, prefer using the `Formatter.isModifier(at:)` helper.
|
|
var isModifierKeyword: Bool {
|
|
switch self {
|
|
case let .keyword(keyword), let .identifier(keyword):
|
|
return _FormatRules.allModifiers.contains(keyword)
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
/// These identifiers are treated as keywords when used in a type position
|
|
var isKeywordInTypeContext: Bool {
|
|
switch self {
|
|
case let .keyword(keyword), let .identifier(keyword):
|
|
return keyword.isKeywordInTypeContext
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|