Skip to content

Commit

Permalink
Sort declarations in each category into subgroupings
Browse files Browse the repository at this point in the history
  • Loading branch information
calda committed Aug 10, 2020
1 parent 430b260 commit d0b99df
Show file tree
Hide file tree
Showing 4 changed files with 218 additions and 107 deletions.
52 changes: 6 additions & 46 deletions Sources/ParsingHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -939,46 +939,6 @@ extension Formatter {
let parser = Formatter(tokens)
var declarations = [Declaration]()

/// Whether or not this token "defines" the specific type of declaration
/// - A valid declaration will include exactly one of these keywords in its outermost scope.
func definesDeclarationType(_ token: Token) -> Bool {
// All of the keywords that map to individual Declaration grammars
// https://docs.swift.org/swift-book/ReferenceManual/Declarations.html#grammar_declaration
let declarationKeywords = ["import", "let", "var", "typealias", "func", "enum", "case",
"struct", "class", "protocol", "init", "deinit",
"extension", "subscript", "operator", "precedencegroup"]

return token.isKeyword && declarationKeywords.contains(token.string)
}

/// Whether or not this token defines the start of a type or type-like scope
/// (e.g. `class`, `struct`, `enum`, `protocol`, and `extension`
func definesType(_ token: Token) -> Bool {
let typeKeywords = ["class", "struct", "enum", "protocol", "extension"]
return token.isKeyword && typeKeywords.contains(token.string)
}

/// Whether or not this token can preceed the token that `definesDeclarationType`
/// in a given declaration. e.g. `public` can preceed `var` in `public var foo = "bar"`.
func canPrecedeDeclarationTypeKeyword(_ token: Token) -> Bool {
/// All of the tokens that can typically preceed the main keyword of a declaration
if token.isAttribute || token.isKeyword || token.isSpaceOrCommentOrLinebreak {
return true
}

// Some tokens are aren't treated as "keywords" by `token.isKeyword`,
// but count as keywords in the context of declarations:
let contextualKeywords = ["convenience", "dynamic", "final", "indirect", "infix", "lazy",
"mutating", "nonmutating", "open", "optional", "override", "postfix",
"precedence", "prefix", "required", "some", "unowned", "weak"]

if token.isIdentifier, contextualKeywords.contains(token.string) {
return true
}

return false
}

while !parser.tokens.isEmpty {
let startOfDeclaration = 0
var endOfDeclaration: Int?
Expand All @@ -996,7 +956,7 @@ extension Formatter {
// Determine the type of declaration and search for where it ends
else if let declarationTypeKeywordIndex = parser.index(
after: startOfDeclaration - 1,
where: definesDeclarationType
where: { $0.definesDeclarationType }
) {
// Search for the next declaration so we know where this declaration ends.
var nextDeclarationKeywordIndex: Int?
Expand All @@ -1009,7 +969,7 @@ extension Formatter {
let endOfScope = parser.endOfScope(at: searchIndex)
{
searchIndex = endOfScope + 1
} else if definesDeclarationType(parser.tokens[searchIndex]) {
} else if parser.tokens[searchIndex].definesDeclarationType {
nextDeclarationKeywordIndex = searchIndex
} else {
searchIndex += 1
Expand All @@ -1023,7 +983,7 @@ extension Formatter {
searchIndex = nextDeclarationKeywordIndex

while searchIndex > declarationTypeKeywordIndex, startOfNextDeclaration == nil {
if canPrecedeDeclarationTypeKeyword(parser.tokens[searchIndex - 1]) {
if parser.tokens[searchIndex - 1].canPrecedeDeclarationTypeKeyword {
searchIndex -= 1
}

Expand All @@ -1045,7 +1005,7 @@ extension Formatter {
of: .nonSpaceOrCommentOrLinebreak,
before: searchIndex
),
!canPrecedeDeclarationTypeKeyword(parser.tokens[previousNonwhitespace])
!parser.tokens[previousNonwhitespace].canPrecedeDeclarationTypeKeyword
{
startOfNextDeclaration = encounteredEndOfScope + 1
}
Expand Down Expand Up @@ -1088,9 +1048,9 @@ extension Formatter {
// If this declaration represents a type, we need to parse its inner declarations as well.
if let declarationTypeKeywordIndex = declarationParser.index(
after: -1,
where: definesDeclarationType
where: { $0.definesDeclarationType }
),
definesType(declarationParser.tokens[declarationTypeKeywordIndex]),
declarationParser.tokens[declarationTypeKeywordIndex].definesType,
let startOfBody = declarationParser.index(of: .startOfScope("{"), after: declarationTypeKeywordIndex),
let endOfBody = declarationParser.endOfScope(at: startOfBody)
{
Expand Down
214 changes: 165 additions & 49 deletions Sources/Rules.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4812,90 +4812,206 @@ public struct _FormatRules {
// TODO: make this customizable
let categoryOrdering = [Category.lifecycle, .open, .public, .internal, .fileprivate, .private]

/// Organizes the flat list of declarations based on category and type
func organize(_ declarations: [Formatter.Declaration]) -> [Formatter.Declaration] {
// Categorize each of the declarations into their primary groups
let categorizedDeclarations: [(declaration: Formatter.Declaration, category: Category?)] = declarations.map { declaration in
switch declaration {
case .comment:
return (declaration, nil)
case let .declaration(tokens), let .type(open: tokens, _, _):
// TODO: Make this customizable
let categorySubordering = [
DeclarationType.staticProperty, .staticPropertyWithBody, .instanceProperty,
.instancePropertyWithBody, .staticMethod, .classMethod, .instanceMethod,
]

if tokens.contains(.keyword("init")) || tokens.contains(.keyword("deinit")) {
return (declaration, .lifecycle)
/// The `Category` of the given `Declaration`
func category(of declaration: Formatter.Declaration) -> Category? {
switch declaration {
case .comment:
return nil

case let .declaration(tokens), let .type(open: tokens, _, _):
if tokens.contains(.keyword("init")) || tokens.contains(.keyword("deinit")) {
return .lifecycle
}

// Search for a visibility keyword,
// making sure we exclude groups like private(set)
let parser = Formatter(tokens)
var searchIndex = 0

while searchIndex < parser.tokens.count {
if parser.tokens[searchIndex].isKeyword,
let visibilityCategory = Category(rawValue: parser.tokens[searchIndex].string),
parser.next(.nonSpaceOrComment, after: searchIndex) != .startOfScope("(")
{
return visibilityCategory
}

// Search for a visibility keyword,
// making sure we exclude groups like private(set)
let parser = Formatter(tokens)
var searchIndex = 0
searchIndex += 1
}

while searchIndex < parser.tokens.count {
if parser.tokens[searchIndex].isKeyword,
let visibilityCategory = Category(rawValue: parser.tokens[searchIndex].string),
parser.next(.nonSpaceOrComment, after: searchIndex) != .startOfScope("(")
{
return (declaration, visibilityCategory)
// `internal` is the default implied vibilility if no other is specified
return .internal
}
}

/// The `DeclarationType` of the given `Declaration`
func type(of declaration: Formatter.Declaration) -> DeclarationType? {
switch declaration {
case .comment, .type:
return nil

case let .declaration(tokens):
let declarationParser = Formatter(tokens)

guard let declarationTypeTokenIndex = declarationParser.index(
after: -1,
where: { $0.definesDeclarationType }
)
else { return nil }

let declarationTypeToken = declarationParser.tokens[declarationTypeTokenIndex]

let isStaticDeclaration = declarationParser.lastToken(
before: declarationTypeTokenIndex,
where: { $0 == .keyword("static") }
) != nil

let isClassDeclaration = declarationParser.lastToken(
before: declarationTypeTokenIndex,
where: { $0 == .keyword("class") }
) != nil

let hasBody: Bool
// If there's an opening bracket and no equals operator,
// then this declaration has a body (e.g. a function body or a computed property body)
if let openingBraceIndex = declarationParser.index(
after: declarationTypeTokenIndex,
where: { $0 == .startOfScope("{") }
) {
hasBody = declarationParser.index(
of: .operator("=", .infix),
in: CountableRange(declarationTypeTokenIndex ... openingBraceIndex)
) == nil
} else {
hasBody = false
}

switch declarationTypeToken {
case .keyword("let"), .keyword("var"):
if isStaticDeclaration {
if hasBody {
return .staticPropertyWithBody
} else {
return .staticProperty
}
} else {
if hasBody {
return .instancePropertyWithBody
} else {
return .instanceProperty
}
}

searchIndex += 1
case .keyword("func"), .keyword("init"), .keyword("deinit"):
if isStaticDeclaration {
return .staticMethod
} else if isClassDeclaration {
return .classMethod
} else {
return .instanceMethod
}

// `internal` is the default implied vibilility if no other is specified
return (declaration, .internal)
default:
return nil
}
}
}

/// Organizes the flat list of declarations based on category and type
func organize(_ declarations: [Formatter.Declaration]) -> [Formatter.Declaration] {
// Categorize each of the declarations into their primary groups
let categorizedDeclarations = declarations.map {
(declaration: $0,
category: category(of: $0),
type: type(of: $0))
}

// Sort the declarations based on their category
var sortedDeclarations = categorizedDeclarations.enumerated().sorted(by: { lhs, rhs in
let (lhsOriginalIndex, (_, lhsCategory)) = lhs
let (rhsOriginalIndex, (_, rhsCategory)) = rhs
let (lhsOriginalIndex, lhs) = lhs
let (rhsOriginalIndex, rhs) = rhs

// Sort primarily by the category sort order
if let lhsCategory = lhsCategory,
let rhsCategory = rhsCategory,
// Sort primarily by category
if let lhsCategory = lhs.category,
let rhsCategory = rhs.category,
let lhsCategorySortOrder = categoryOrdering.index(of: lhsCategory),
let rhsCategorySortOrder = categoryOrdering.index(of: rhsCategory),
lhsCategorySortOrder != rhsCategorySortOrder
{
return lhsCategorySortOrder < rhsCategorySortOrder
}

// Respect the original declaration ordering when the categories are the same
// Within individual categories, sort by the declaration type
if let lhsType = lhs.type,
let rhsType = rhs.type,
let lhsTypeSortOrder = categorySubordering.index(of: lhsType),
let rhsTypeSortOrder = categorySubordering.index(of: rhsType),
lhsTypeSortOrder != rhsTypeSortOrder
{
return lhsTypeSortOrder < rhsTypeSortOrder
}

// Respect the original declaration ordering when the categories and types are the same
return lhsOriginalIndex < rhsOriginalIndex
}).map { $0.element }

// Insert comments to separate the categories
func indexOfFirstDeclaration(in category: Category) -> Int? {
sortedDeclarations.firstIndex(where: { $0.category == category })
}

for category in categoryOrdering {
guard let indexOfFirstDeclaration = indexOfFirstDeclaration(in: category) else {
continue
}
guard let indexOfFirstDeclaration = sortedDeclarations
.firstIndex(where: { $0.category == category })
else { continue }

let firstDeclaration = sortedDeclarations[indexOfFirstDeclaration].declaration
let declarationParser = Formatter(firstDeclaration.tokens)
let indentation = declarationParser.indentForLine(at: 0)

let markComment = "// MARK: \(category.rawValue.capitalized)"
let markDeclaration = tokenize("\(indentation)// MARK: \(category.rawValue.capitalized)\n\n")
sortedDeclarations.insert((.comment(markDeclaration), nil, nil), at: indexOfFirstDeclaration)

// Verify we don't already have a mark comment here
// TODO: Harden this with test cases
if firstDeclaration.tokens.map({ $0.string }).joined().contains(markComment) {
continue
}
// Insert newlines to separate declaration types
for declarationType in categorySubordering {
guard let indexOfLastDeclarationWithType = sortedDeclarations
.lastIndex(where: { $0.category == category && $0.type == declarationType })
else { continue }

if indexOfFirstDeclaration != 0 {
let previousDeclaration = sortedDeclarations[indexOfFirstDeclaration - 1].declaration
if previousDeclaration.tokens.map({ $0.string }).joined().contains(markComment) {
switch sortedDeclarations[indexOfLastDeclarationWithType].declaration {
case .comment, .type:
continue

case let .declaration(tokens):
let lastDeclarationParser = Formatter(tokens)

// Determine how many trailing linebreaks there are in this declaration
var numberOfTrailingLinebreaks = 0
var searchIndex = lastDeclarationParser.tokens.count - 1

while searchIndex > 0,
let token = lastDeclarationParser.token(at: searchIndex),
token.isSpaceOrCommentOrLinebreak
{
if token.isLinebreak {
numberOfTrailingLinebreaks += 1
}

searchIndex -= 1
}

// Make sure there are atleast two newlines,
// so we get a blank line between individual declaration types
while numberOfTrailingLinebreaks < 2 {
lastDeclarationParser.insertLinebreak(at: lastDeclarationParser.tokens.count)
numberOfTrailingLinebreaks += 1
}

sortedDeclarations[indexOfLastDeclarationWithType].declaration = .declaration(lastDeclarationParser.tokens)
}
}

let markDeclaration = tokenize("\(indentation)\(markComment)\n\n")
sortedDeclarations.insert((.comment(markDeclaration), nil), at: indexOfFirstDeclaration)
}

return sortedDeclarations.map { $0.declaration }
Expand Down Expand Up @@ -4925,7 +5041,7 @@ public struct _FormatRules {
.map { organizeBody(of: $0) }

let updatedTokens = organizedDeclarations.flatMap { $0.tokens }

formatter.replaceTokens(
inRange: 0 ..< formatter.tokens.count,
with: updatedTokens
Expand Down
Loading

0 comments on commit d0b99df

Please sign in to comment.