Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Sources/SwiftFormat/Pipelines+Generated.swift
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,7 @@ class LintPipeline: SyntaxVisitor {

override func visit(_ node: SourceFileSyntax) -> SyntaxVisitorContinueKind {
visitIfEnabled(AmbiguousTrailingClosureOverload.visit, in: context, for: node)
visitIfEnabled(FileprivateAtFileScope.visit, in: context, for: node)
visitIfEnabled(NeverForceUnwrap.visit, in: context, for: node)
visitIfEnabled(NeverUseForceTry.visit, in: context, for: node)
visitIfEnabled(NeverUseImplicitlyUnwrappedOptionals.visit, in: context, for: node)
Expand Down Expand Up @@ -286,6 +287,7 @@ extension FormatPipeline {
func visit(_ node: Syntax) -> Syntax {
var node = node
node = DoNotUseSemicolons(context: context).visit(node)
node = FileprivateAtFileScope(context: context).visit(node)
node = FullyIndirectEnum(context: context).visit(node)
node = GroupNumericLiterals(context: context).visit(node)
node = NoAccessLevelOnExtensionDeclaration(context: context).visit(node)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ enum RuleRegistry {
"BeginDocumentationCommentWithOneLineSummary": true,
"DoNotUseSemicolons": true,
"DontRepeatTypeInStaticProperties": true,
"FileprivateAtFileScope": true,
"FullyIndirectEnum": true,
"GroupNumericLiterals": true,
"IdentifiersMustBeASCII": true,
Expand Down
162 changes: 162 additions & 0 deletions Sources/SwiftFormatRules/FileprivateAtFileScope.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import SwiftFormatCore
import SwiftSyntax

/// Declarations at file scope should be declared `fileprivate`, not `private`.
///
/// Using `private` at file scope actually gives the declaration `fileprivate` visibility, so using
/// `fileprivate` explicitly is a better indicator of intent.
///
/// Lint: If a file-scoped declaration has `private` visibility, a lint error is raised.
///
/// Format: File-scoped declarations that have `private` visibility will have their visibility
/// changed to `fileprivate`.
public final class FileprivateAtFileScope: SyntaxFormatRule {
public override func visit(_ node: SourceFileSyntax) -> Syntax {
let newStatements = rewrittenCodeBlockItems(node.statements)
return Syntax(node.withStatements(newStatements))
}

/// Returns a list of code block items equivalent to the given list, but where any file-scoped
/// declarations have had their `private` modifier replaced by `fileprivate` if present.
///
/// - Parameter codeBlockItems: The list of code block items to rewrite.
/// - Returns: A new `CodeBlockItemListSyntax` that has possibly been rewritten.
private func rewrittenCodeBlockItems(_ codeBlockItems: CodeBlockItemListSyntax)
-> CodeBlockItemListSyntax
{
let newCodeBlockItems = codeBlockItems.map { codeBlockItem -> CodeBlockItemSyntax in
switch codeBlockItem.item.as(SyntaxEnum.self) {
case .ifConfigDecl(let ifConfigDecl):
// We need to look through `#if/#elseif/#else` blocks because the decls directly inside
// them are still considered file-scope for our purposes.
return codeBlockItem.withItem(Syntax(rewrittenIfConfigDecl(ifConfigDecl)))

case .functionDecl(let functionDecl):
return codeBlockItem.withItem(
Syntax(rewrittenDecl(
functionDecl,
modifiers: functionDecl.modifiers,
factory: functionDecl.withModifiers)))

case .variableDecl(let variableDecl):
return codeBlockItem.withItem(
Syntax(rewrittenDecl(
variableDecl,
modifiers: variableDecl.modifiers,
factory: variableDecl.withModifiers)))

case .classDecl(let classDecl):
return codeBlockItem.withItem(
Syntax(rewrittenDecl(
classDecl,
modifiers: classDecl.modifiers,
factory: classDecl.withModifiers)))

case .structDecl(let structDecl):
return codeBlockItem.withItem(
Syntax(rewrittenDecl(
structDecl,
modifiers: structDecl.modifiers,
factory: structDecl.withModifiers)))

case .enumDecl(let enumDecl):
return codeBlockItem.withItem(
Syntax(rewrittenDecl(
enumDecl,
modifiers: enumDecl.modifiers,
factory: enumDecl.withModifiers)))

case .protocolDecl(let protocolDecl):
return codeBlockItem.withItem(
Syntax(rewrittenDecl(
protocolDecl,
modifiers: protocolDecl.modifiers,
factory: protocolDecl.withModifiers)))

case .typealiasDecl(let typealiasDecl):
return codeBlockItem.withItem(
Syntax(rewrittenDecl(
typealiasDecl,
modifiers: typealiasDecl.modifiers,
factory: typealiasDecl.withModifiers)))

case .extensionDecl(let extensionDecl):
return codeBlockItem.withItem(
Syntax(rewrittenDecl(
extensionDecl,
modifiers: extensionDecl.modifiers,
factory: extensionDecl.withModifiers)))

default:
return codeBlockItem
}
}
return SyntaxFactory.makeCodeBlockItemList(newCodeBlockItems)
}

/// Returns a new `IfConfigDeclSyntax` equivalent to the given node, but where any file-scoped
/// declarations have had their `private` modifier replaced by `fileprivate` if present.
///
/// - Parameter ifConfigDecl: The `IfConfigDeclSyntax` to rewrite.
/// - Returns: A new `IfConfigDeclSyntax` that has possibly been rewritten.
private func rewrittenIfConfigDecl(_ ifConfigDecl: IfConfigDeclSyntax) -> IfConfigDeclSyntax {
let newClauses = ifConfigDecl.clauses.map { clause -> IfConfigClauseSyntax in
switch clause.elements.as(SyntaxEnum.self) {
case .codeBlockItemList(let codeBlockItemList):
return clause.withElements(Syntax(rewrittenCodeBlockItems(codeBlockItemList)))
default:
return clause
}
}
return ifConfigDecl.withClauses(SyntaxFactory.makeIfConfigClauseList(newClauses))
}

/// Returns a rewritten version of the given declaration if its modifier list contains `private`
/// that contains `fileprivate` instead.
///
/// If the modifier list does not contain `private`, the original declaration is returned
/// unchanged.
///
/// - Parameters:
/// - decl: The declaration to possibly rewrite.
/// - modifiers: The modifier list of the declaration (i.e., `decl.modifiers`).
/// - factory: A reference to the `decl`'s `withModifiers` instance method that is called to
/// rewrite the node if needed.
/// - Returns: A new node if the modifiers were rewritten, or the original node if not.
private func rewrittenDecl<DeclType: DeclSyntaxProtocol>(
_ decl: DeclType,
modifiers: ModifierListSyntax?,
factory: (ModifierListSyntax?) -> DeclType
) -> DeclType {
guard let modifiers = modifiers, modifiers.has(modifier: "private") else {
return decl
}

let newModifiers = modifiers.map { modifier -> DeclModifierSyntax in
let name = modifier.name
if name.tokenKind == .privateKeyword {
diagnose(.replacePrivateWithFileprivate, on: name)
return modifier.withName(name.withKind(.fileprivateKeyword))
}
return modifier
}
return factory(SyntaxFactory.makeModifierList(newModifiers))
}
}

extension Diagnostic.Message {
public static let replacePrivateWithFileprivate =
Diagnostic.Message(.warning, "replace 'private' with 'fileprivate' on file-scoped declarations")
}
191 changes: 191 additions & 0 deletions Tests/SwiftFormatRulesTests/FileprivateAtFileScopeTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import SwiftFormatRules

final class FileprivateAtFileScopeTests: DiagnosingTestCase {
func testFileScopeDecls() {
XCTAssertFormatting(
FileprivateAtFileScope.self,
input: """
private class Foo {}
private struct Foo {}
private enum Foo {}
private protocol Foo {}
private typealias Foo = Bar
private extension Foo {}
private func foo() {}
private var foo: Bar
""",
expected: """
fileprivate class Foo {}
fileprivate struct Foo {}
fileprivate enum Foo {}
fileprivate protocol Foo {}
fileprivate typealias Foo = Bar
fileprivate extension Foo {}
fileprivate func foo() {}
fileprivate var foo: Bar
""")
XCTAssertDiagnosed(.replacePrivateWithFileprivate)
XCTAssertDiagnosed(.replacePrivateWithFileprivate)
XCTAssertDiagnosed(.replacePrivateWithFileprivate)
XCTAssertDiagnosed(.replacePrivateWithFileprivate)
XCTAssertDiagnosed(.replacePrivateWithFileprivate)
XCTAssertDiagnosed(.replacePrivateWithFileprivate)
XCTAssertDiagnosed(.replacePrivateWithFileprivate)
XCTAssertDiagnosed(.replacePrivateWithFileprivate)
}

func testNonFileScopeDeclsAreNotChanged() {
XCTAssertFormatting(
FileprivateAtFileScope.self,
input: """
enum Namespace {
private class Foo {}
private struct Foo {}
private enum Foo {}
private typealias Foo = Bar
private static func foo() {}
private static var foo: Bar
}
""",
expected: """
enum Namespace {
private class Foo {}
private struct Foo {}
private enum Foo {}
private typealias Foo = Bar
private static func foo() {}
private static var foo: Bar
}
""")
XCTAssertNotDiagnosed(.replacePrivateWithFileprivate)
}

func testFileScopeDeclsInsideConditionals() {
XCTAssertFormatting(
FileprivateAtFileScope.self,
input: """
#if FOO
private class Foo {}
private struct Foo {}
private enum Foo {}
private protocol Foo {}
private typealias Foo = Bar
private extension Foo {}
private func foo() {}
private var foo: Bar
#elseif BAR
private class Foo {}
private struct Foo {}
private enum Foo {}
private protocol Foo {}
private typealias Foo = Bar
private extension Foo {}
private func foo() {}
private var foo: Bar
#else
private class Foo {}
private struct Foo {}
private enum Foo {}
private protocol Foo {}
private typealias Foo = Bar
private extension Foo {}
private func foo() {}
private var foo: Bar
#endif
""",
expected: """
#if FOO
fileprivate class Foo {}
fileprivate struct Foo {}
fileprivate enum Foo {}
fileprivate protocol Foo {}
fileprivate typealias Foo = Bar
fileprivate extension Foo {}
fileprivate func foo() {}
fileprivate var foo: Bar
#elseif BAR
fileprivate class Foo {}
fileprivate struct Foo {}
fileprivate enum Foo {}
fileprivate protocol Foo {}
fileprivate typealias Foo = Bar
fileprivate extension Foo {}
fileprivate func foo() {}
fileprivate var foo: Bar
#else
fileprivate class Foo {}
fileprivate struct Foo {}
fileprivate enum Foo {}
fileprivate protocol Foo {}
fileprivate typealias Foo = Bar
fileprivate extension Foo {}
fileprivate func foo() {}
fileprivate var foo: Bar
#endif
""")
}

func testFileScopeDeclsInsideNestedConditionals() {
XCTAssertFormatting(
FileprivateAtFileScope.self,
input: """
#if FOO
#if BAR
private class Foo {}
private struct Foo {}
private enum Foo {}
private protocol Foo {}
private typealias Foo = Bar
private extension Foo {}
private func foo() {}
private var foo: Bar
#endif
#endif
""",
expected: """
#if FOO
#if BAR
fileprivate class Foo {}
fileprivate struct Foo {}
fileprivate enum Foo {}
fileprivate protocol Foo {}
fileprivate typealias Foo = Bar
fileprivate extension Foo {}
fileprivate func foo() {}
fileprivate var foo: Bar
#endif
#endif
""")
}

func testLeadingTriviaIsPreserved() {
XCTAssertFormatting(
FileprivateAtFileScope.self,
input: """
/// Some doc comment
private class Foo {}

@objc /* comment */ private class Bar {}
""",
expected: """
/// Some doc comment
fileprivate class Foo {}

@objc /* comment */ fileprivate class Bar {}
""")
XCTAssertDiagnosed(.replacePrivateWithFileprivate)
XCTAssertDiagnosed(.replacePrivateWithFileprivate)
}

func testModifierDetailIsPreserved() {
XCTAssertFormatting(
FileprivateAtFileScope.self,
input: """
public private(set) var foo: Int
""",
expected: """
public fileprivate(set) var foo: Int
""")
XCTAssertDiagnosed(.replacePrivateWithFileprivate)
}
}
Loading