mirror of
https://github.com/nicklockwood/SwiftFormat.git
synced 2026-05-17 10:30:35 +00:00
234 lines
9.7 KiB
Swift
234 lines
9.7 KiB
Swift
//
|
|
// RedundantFileprivate.swift
|
|
// SwiftFormat
|
|
//
|
|
// Created by Nick Lockwood on 2/3/19.
|
|
// Copyright © 2024 Nick Lockwood. All rights reserved.
|
|
//
|
|
|
|
import Foundation
|
|
|
|
public extension FormatRule {
|
|
/// Replace `fileprivate` with `private` where possible
|
|
static let redundantFileprivate = FormatRule(
|
|
help: "Prefer `private` over `fileprivate` where equivalent."
|
|
) { formatter in
|
|
guard !formatter.options.fragment else { return }
|
|
|
|
var hasUnreplacedFileprivates = false
|
|
formatter.forEach(.keyword("fileprivate")) { i, _ in
|
|
// check if definition is at file-scope
|
|
if formatter.index(of: .startOfScope, before: i) == nil {
|
|
formatter.replaceToken(at: i, with: .keyword("private"))
|
|
} else {
|
|
hasUnreplacedFileprivates = true
|
|
}
|
|
}
|
|
guard hasUnreplacedFileprivates else {
|
|
return
|
|
}
|
|
let importRanges = formatter.parseImports()
|
|
var fileJustContainsOneType: Bool?
|
|
|
|
formatter.forEach(.keyword("fileprivate")) { i, _ in
|
|
// Check if definition is a member of a file-scope type
|
|
guard formatter.options.swiftVersion >= "4",
|
|
let scopeIndex = formatter.index(of: .startOfScope, before: i, if: {
|
|
$0 == .startOfScope("{")
|
|
}), let typeIndex = formatter.index(of: .keyword, before: scopeIndex, if: {
|
|
["class", "actor", "struct", "enum", "extension"].contains($0.string)
|
|
}), let nameIndex = formatter.index(of: .identifier, in: typeIndex ..< scopeIndex),
|
|
formatter.next(.nonSpaceOrCommentOrLinebreak, after: nameIndex)?.isOperator(".") == false,
|
|
case let .identifier(typeName) = formatter.tokens[nameIndex],
|
|
let endIndex = formatter.index(of: .endOfScope, after: scopeIndex),
|
|
formatter.currentScope(at: typeIndex) == nil
|
|
else {
|
|
return
|
|
}
|
|
// Get member type
|
|
guard let keywordIndex = formatter.index(of: .keyword, in: i + 1 ..< endIndex),
|
|
let memberType = formatter.declarationType(at: keywordIndex),
|
|
// TODO: check if member types are exposed in the interface, otherwise convert them too
|
|
["let", "var", "func", "init"].contains(memberType)
|
|
else {
|
|
return
|
|
}
|
|
// Check that type doesn't (potentially) conform to a protocol
|
|
// TODO: use a whitelist of known protocols to make this check less blunt
|
|
guard !formatter.tokens[typeIndex ..< scopeIndex].contains(.delimiter(":")) else {
|
|
return
|
|
}
|
|
// Check for code outside of main type definition
|
|
let startIndex = formatter.startOfModifiers(at: typeIndex, includingAttributes: true)
|
|
if fileJustContainsOneType == nil {
|
|
fileJustContainsOneType = !formatter.ifCodeInRange(0 ..< startIndex, importRanges: importRanges) &&
|
|
!formatter.ifCodeInRange(endIndex + 1 ..< formatter.tokens.count, importRanges: importRanges)
|
|
}
|
|
if fileJustContainsOneType == true {
|
|
formatter.replaceToken(at: i, with: .keyword("private"))
|
|
return
|
|
}
|
|
// Check if type name is initialized outside type, and if so don't
|
|
// change any fileprivate members in case we break memberwise initializer
|
|
// TODO: check if struct contains an overridden init; if so we can skip this check
|
|
if formatter.tokens[typeIndex] == .keyword("struct"),
|
|
formatter.isTypeInitialized(typeName, in: 0 ..< startIndex) ||
|
|
formatter.isTypeInitialized(typeName, in: endIndex + 1 ..< formatter.tokens.count)
|
|
{
|
|
return
|
|
}
|
|
// Check if member is referenced outside type
|
|
if memberType == "init" {
|
|
// Make initializer private if it's not called anywhere
|
|
if !formatter.isTypeInitialized(typeName, in: 0 ..< startIndex),
|
|
!formatter.isTypeInitialized(typeName, in: endIndex + 1 ..< formatter.tokens.count),
|
|
!formatter.isInitOverridden(for: typeName, in: 0 ..< startIndex),
|
|
!formatter.isInitOverridden(for: typeName, in: endIndex + 1 ..< formatter.tokens.count)
|
|
{
|
|
formatter.replaceToken(at: i, with: .keyword("private"))
|
|
}
|
|
} else if let _names = formatter.namesInDeclaration(at: keywordIndex),
|
|
case let names = _names + _names.map({ "$\($0)" }),
|
|
!formatter.areMembers(names, of: typeName, referencedIn: 0 ..< startIndex),
|
|
!formatter.areMembers(names, of: typeName, referencedIn: endIndex + 1 ..< formatter.tokens.count)
|
|
{
|
|
formatter.replaceToken(at: i, with: .keyword("private"))
|
|
}
|
|
}
|
|
} examples: {
|
|
"""
|
|
```diff
|
|
- fileprivate let someConstant = "someConstant"
|
|
+ private let someConstant = "someConstant"
|
|
```
|
|
|
|
In Swift 4 and above, `fileprivate` can also be replaced with `private` for
|
|
members that are only accessed from extensions in the same file:
|
|
|
|
```diff
|
|
class Foo {
|
|
- fileprivate var foo = "foo"
|
|
+ private var foo = "foo"
|
|
}
|
|
|
|
extension Foo {
|
|
func bar() {
|
|
print(self.foo)
|
|
}
|
|
}
|
|
```
|
|
"""
|
|
}
|
|
}
|
|
|
|
extension Formatter {
|
|
func ifCodeInRange(_ range: CountableRange<Int>, importRanges: [[ImportRange]]) -> Bool {
|
|
var index = range.lowerBound
|
|
while index < range.upperBound, let nextIndex =
|
|
self.index(of: .nonSpaceOrCommentOrLinebreak, in: index ..< range.upperBound)
|
|
{
|
|
guard let importRange = importRanges.first(where: {
|
|
$0.contains(where: { $0.range.contains(nextIndex) })
|
|
}) else {
|
|
return true
|
|
}
|
|
index = importRange.last!.range.upperBound + 1
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isTypeInitialized(_ name: String, in range: CountableRange<Int>) -> Bool {
|
|
for i in range {
|
|
switch tokens[i] {
|
|
case .identifier(name):
|
|
guard let nextIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: i) else {
|
|
break
|
|
}
|
|
switch tokens[nextIndex] {
|
|
case .operator(".", .infix):
|
|
if next(.nonSpaceOrCommentOrLinebreak, after: nextIndex) == .identifier("init") {
|
|
return true
|
|
}
|
|
case .startOfScope("("):
|
|
return true
|
|
case .startOfScope("{"):
|
|
if isStartOfClosure(at: nextIndex) {
|
|
return true
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
case .identifier("init"):
|
|
// TODO: this will return true if *any* type is initialized using type inference.
|
|
// Is there a way to narrow this down a bit?
|
|
if last(.nonSpaceOrCommentOrLinebreak, before: i) == .operator(".", .prefix) {
|
|
return true
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// TODO: improve this logic to handle shadowing
|
|
func areMembers(_ names: [String], of type: String,
|
|
referencedIn range: CountableRange<Int>) -> Bool
|
|
{
|
|
var i = range.lowerBound
|
|
while i < range.upperBound {
|
|
switch tokens[i] {
|
|
case .keyword("struct"), .keyword("extension"), .keyword("enum"), .keyword("actor"),
|
|
.keyword("class") where declarationType(at: i) == "class":
|
|
guard let startIndex = index(of: .startOfScope("{"), after: i),
|
|
let endIndex = endOfScope(at: startIndex)
|
|
else {
|
|
break
|
|
}
|
|
guard let nameIndex = index(of: .nonSpaceOrCommentOrLinebreak, after: i),
|
|
tokens[nameIndex] != .identifier(type)
|
|
else {
|
|
i = endIndex
|
|
break
|
|
}
|
|
for case let .identifier(name) in tokens[startIndex ..< endIndex]
|
|
where names.contains(name)
|
|
{
|
|
return true
|
|
}
|
|
i = endIndex
|
|
case let .identifier(name) where names.contains(name):
|
|
if let dotIndex = index(of: .nonSpaceOrCommentOrLinebreak, before: i, if: {
|
|
$0 == .operator(".", .infix)
|
|
}), last(.nonSpaceOrCommentOrLinebreak, before: dotIndex)
|
|
!= .identifier("self")
|
|
{
|
|
return true
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
i += 1
|
|
}
|
|
return false
|
|
}
|
|
|
|
func isInitOverridden(for type: String, in range: CountableRange<Int>) -> Bool {
|
|
for i in range {
|
|
if case .keyword("init") = tokens[i],
|
|
let scopeStart = index(of: .startOfScope("{"), after: i),
|
|
index(of: .identifier("super"), after: scopeStart) != nil,
|
|
let scopeIndex = index(of: .startOfScope("{"), before: i),
|
|
let colonIndex = index(of: .delimiter(":"), before: scopeIndex),
|
|
next(
|
|
.nonSpaceOrCommentOrLinebreak,
|
|
in: colonIndex + 1 ..< scopeIndex
|
|
) == .identifier(type)
|
|
{
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
}
|