Skip to content

Commit 41e0bd2

Browse files
authored
Merge pull request swiftlang#152 from allevato/fileprivate-configurable
Make the `FileprivateAtFileScope` rule configurable.
2 parents 200fa5a + e1e7ee5 commit 41e0bd2

File tree

7 files changed

+315
-213
lines changed

7 files changed

+315
-213
lines changed

Sources/SwiftFormat/Pipelines+Generated.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ class LintPipeline: SyntaxVisitor {
206206

207207
override func visit(_ node: SourceFileSyntax) -> SyntaxVisitorContinueKind {
208208
visitIfEnabled(AmbiguousTrailingClosureOverload.visit, in: context, for: node)
209-
visitIfEnabled(FileprivateAtFileScope.visit, in: context, for: node)
209+
visitIfEnabled(FileScopedDeclarationPrivacy.visit, in: context, for: node)
210210
visitIfEnabled(NeverForceUnwrap.visit, in: context, for: node)
211211
visitIfEnabled(NeverUseForceTry.visit, in: context, for: node)
212212
visitIfEnabled(NeverUseImplicitlyUnwrappedOptionals.visit, in: context, for: node)
@@ -286,7 +286,7 @@ extension FormatPipeline {
286286
func visit(_ node: Syntax) -> Syntax {
287287
var node = node
288288
node = DoNotUseSemicolons(context: context).visit(node)
289-
node = FileprivateAtFileScope(context: context).visit(node)
289+
node = FileScopedDeclarationPrivacy(context: context).visit(node)
290290
node = FullyIndirectEnum(context: context).visit(node)
291291
node = GroupNumericLiterals(context: context).visit(node)
292292
node = NoAccessLevelOnExtensionDeclaration(context: context).visit(node)

Sources/SwiftFormatConfiguration/Configuration.swift

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public struct Configuration: Codable, Equatable {
3232
case prioritizeKeepingFunctionOutputTogether
3333
case indentConditionalCompilationBlocks
3434
case lineBreakAroundMultilineExpressionChainComponents
35+
case fileScopedDeclarationPrivacy
3536
case rules
3637
}
3738

@@ -115,6 +116,10 @@ public struct Configuration: Codable, Equatable {
115116
/// (i.e. right paren, right bracket, right brace, etc.).
116117
public var lineBreakAroundMultilineExpressionChainComponents = false
117118

119+
/// Determines the formal access level (i.e., the level specified in source code) for file-scoped
120+
/// declarations whose effective access level is private to the containing file.
121+
public var fileScopedDeclarationPrivacy = FileScopedDeclarationPrivacyConfiguration()
122+
118123
/// Constructs a Configuration with all default values.
119124
public init() {
120125
self.version = highestSupportedConfigurationVersion
@@ -167,6 +172,10 @@ public struct Configuration: Codable, Equatable {
167172
self.lineBreakAroundMultilineExpressionChainComponents =
168173
try container.decodeIfPresent(
169174
Bool.self, forKey: .lineBreakAroundMultilineExpressionChainComponents) ?? false
175+
self.fileScopedDeclarationPrivacy =
176+
try container.decodeIfPresent(
177+
FileScopedDeclarationPrivacyConfiguration.self, forKey: .fileScopedDeclarationPrivacy)
178+
?? FileScopedDeclarationPrivacyConfiguration()
170179

171180
// If the `rules` key is not present at all, default it to the built-in set
172181
// so that the behavior is the same as if the configuration had been
@@ -193,6 +202,7 @@ public struct Configuration: Codable, Equatable {
193202
try container.encode(
194203
lineBreakAroundMultilineExpressionChainComponents,
195204
forKey: .lineBreakAroundMultilineExpressionChainComponents)
205+
try container.encode(fileScopedDeclarationPrivacy, forKey: .fileScopedDeclarationPrivacy)
196206
try container.encode(rules, forKey: .rules)
197207
}
198208

@@ -234,3 +244,24 @@ public struct NoPlaygroundLiteralsConfiguration: Codable, Equatable {
234244
/// Resolution behavior to use when encountering an ambiguous `#colorLiteral`.
235245
public let resolveAmbiguousColor: ResolveBehavior = .useUIColor
236246
}
247+
248+
/// Configuration for the `FileScopedDeclarationPrivacy` rule.
249+
public struct FileScopedDeclarationPrivacyConfiguration: Codable, Equatable {
250+
public enum AccessLevel: String, Codable {
251+
/// Private file-scoped declarations should be declared `private`.
252+
///
253+
/// If a file-scoped declaration is declared `fileprivate`, it will be diagnosed (in lint mode)
254+
/// or changed to `private` (in format mode).
255+
case `private`
256+
257+
/// Private file-scoped declarations should be declared `fileprivate`.
258+
///
259+
/// If a file-scoped declaration is declared `private`, it will be diagnosed (in lint mode) or
260+
/// changed to `fileprivate` (in format mode).
261+
case `fileprivate`
262+
}
263+
264+
/// The formal access level to use when encountering a file-scoped declaration with effective
265+
/// private access.
266+
public var accessLevel: AccessLevel = .private
267+
}

Sources/SwiftFormatRules/FileprivateAtFileScope.swift renamed to Sources/SwiftFormatRules/FileScopedDeclarationPrivacy.swift

Lines changed: 37 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,23 +13,24 @@
1313
import SwiftFormatCore
1414
import SwiftSyntax
1515

16-
/// Declarations at file scope should be declared `fileprivate`, not `private`.
16+
/// Declarations at file scope with effective private access should be consistently declared as
17+
/// either `fileprivate` or `private`, determined by configuration.
1718
///
18-
/// Using `private` at file scope actually gives the declaration `fileprivate` visibility, so using
19-
/// `fileprivate` explicitly is a better indicator of intent.
19+
/// Lint: If a file-scoped declaration has formal access opposite to the desired access level in the
20+
/// formatter's configuration, a lint error is raised.
2021
///
21-
/// Lint: If a file-scoped declaration has `private` visibility, a lint error is raised.
22-
///
23-
/// Format: File-scoped declarations that have `private` visibility will have their visibility
24-
/// changed to `fileprivate`.
25-
public final class FileprivateAtFileScope: SyntaxFormatRule {
22+
/// Format: File-scoped declarations that have formal access opposite to the desired access level in
23+
/// the formatter's configuration will have their access level changed.
24+
public final class FileScopedDeclarationPrivacy: SyntaxFormatRule {
2625
public override func visit(_ node: SourceFileSyntax) -> Syntax {
2726
let newStatements = rewrittenCodeBlockItems(node.statements)
2827
return Syntax(node.withStatements(newStatements))
2928
}
3029

3130
/// Returns a list of code block items equivalent to the given list, but where any file-scoped
32-
/// declarations have had their `private` modifier replaced by `fileprivate` if present.
31+
/// declarations with effective private access have had their formal access level rewritten, if
32+
/// necessary, to be either `private` or `fileprivate`, as determined by the formatter
33+
/// configuration.
3334
///
3435
/// - Parameter codeBlockItems: The list of code block items to rewrite.
3536
/// - Returns: A new `CodeBlockItemListSyntax` that has possibly been rewritten.
@@ -100,7 +101,9 @@ public final class FileprivateAtFileScope: SyntaxFormatRule {
100101
}
101102

102103
/// Returns a new `IfConfigDeclSyntax` equivalent to the given node, but where any file-scoped
103-
/// declarations have had their `private` modifier replaced by `fileprivate` if present.
104+
/// declarations with effective private access have had their formal access level rewritten, if
105+
/// necessary, to be either `private` or `fileprivate`, as determined by the formatter
106+
/// configuration.
104107
///
105108
/// - Parameter ifConfigDecl: The `IfConfigDeclSyntax` to rewrite.
106109
/// - Returns: A new `IfConfigDeclSyntax` that has possibly been rewritten.
@@ -119,8 +122,8 @@ public final class FileprivateAtFileScope: SyntaxFormatRule {
119122
/// Returns a rewritten version of the given declaration if its modifier list contains `private`
120123
/// that contains `fileprivate` instead.
121124
///
122-
/// If the modifier list does not contain `private`, the original declaration is returned
123-
/// unchanged.
125+
/// If the modifier list is not inconsistent with the configured access level, the original
126+
/// declaration is returned unchanged.
124127
///
125128
/// - Parameters:
126129
/// - decl: The declaration to possibly rewrite.
@@ -133,15 +136,30 @@ public final class FileprivateAtFileScope: SyntaxFormatRule {
133136
modifiers: ModifierListSyntax?,
134137
factory: (ModifierListSyntax?) -> DeclType
135138
) -> DeclType {
136-
guard let modifiers = modifiers, modifiers.has(modifier: "private") else {
139+
let invalidAccess: TokenKind
140+
let validAccess: TokenKind
141+
let diagnostic: Diagnostic.Message
142+
143+
switch context.configuration.fileScopedDeclarationPrivacy.accessLevel {
144+
case .private:
145+
invalidAccess = .fileprivateKeyword
146+
validAccess = .privateKeyword
147+
diagnostic = .replaceFileprivateWithPrivate
148+
case .fileprivate:
149+
invalidAccess = .privateKeyword
150+
validAccess = .fileprivateKeyword
151+
diagnostic = .replacePrivateWithFileprivate
152+
}
153+
154+
guard let modifiers = modifiers, modifiers.has(modifier: invalidAccess) else {
137155
return decl
138156
}
139157

140158
let newModifiers = modifiers.map { modifier -> DeclModifierSyntax in
141159
let name = modifier.name
142-
if name.tokenKind == .privateKeyword {
143-
diagnose(.replacePrivateWithFileprivate, on: name)
144-
return modifier.withName(name.withKind(.fileprivateKeyword))
160+
if name.tokenKind == invalidAccess {
161+
diagnose(diagnostic, on: name)
162+
return modifier.withName(name.withKind(validAccess))
145163
}
146164
return modifier
147165
}
@@ -152,4 +170,7 @@ public final class FileprivateAtFileScope: SyntaxFormatRule {
152170
extension Diagnostic.Message {
153171
public static let replacePrivateWithFileprivate =
154172
Diagnostic.Message(.warning, "replace 'private' with 'fileprivate' on file-scoped declarations")
173+
174+
public static let replaceFileprivateWithPrivate =
175+
Diagnostic.Message(.warning, "replace 'fileprivate' with 'private' on file-scoped declarations")
155176
}

Sources/SwiftFormatRules/ModifierListSyntax+Convenience.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ extension ModifierListSyntax {
1818
return contains { $0.name.text == modifier }
1919
}
2020

21+
func has(modifier: TokenKind) -> Bool {
22+
return contains { $0.name.tokenKind == modifier }
23+
}
24+
2125
/// Returns the declaration's access level modifier, if present.
2226
var accessLevelModifier: DeclModifierSyntax? {
2327
for modifier in self {

0 commit comments

Comments
 (0)