Skip to content

Commit 90df711

Browse files
LaurenWhiteallevato
authored andcommitted
Implement no access level on extension declaration (swiftlang#57)
1 parent 6405b30 commit 90df711

File tree

3 files changed

+278
-0
lines changed

3 files changed

+278
-0
lines changed

Diff for: Sources/Rules/AddModifierRewriter.swift

+151
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import Core
2+
import Foundation
3+
import SwiftSyntax
4+
5+
private final class AddModifierRewriter: SyntaxRewriter {
6+
let modifierKeyword: DeclModifierSyntax
7+
8+
init(modifierKeyword: DeclModifierSyntax) {
9+
self.modifierKeyword = modifierKeyword
10+
}
11+
12+
13+
override func visit(_ node: VariableDeclSyntax) -> DeclSyntax {
14+
// Check for modifiers, if none, put accessor keyword before the first token
15+
guard let modifiers = node.modifiers else {
16+
guard let newDecl = removeFirstTokLeadingTrivia(node: node)
17+
as? VariableDeclSyntax else { return node }
18+
return newDecl.addModifier(modifierKeyword)
19+
}
20+
// If variable already has an accessor keyword, skip (do not overwrite)
21+
guard !hasAccessorKeyword(modifiers: modifiers) else { return node }
22+
23+
// Put accessor keyword before the first modifier keyword in the declaration
24+
let newModifiers = insertAccessorKeyword(curModifiers: modifiers)
25+
return node.withModifiers(newModifiers)
26+
}
27+
28+
override func visit(_ node: FunctionDeclSyntax) -> DeclSyntax {
29+
guard let modifiers = node.modifiers else {
30+
guard let newDecl = removeFirstTokLeadingTrivia(node: node)
31+
as? FunctionDeclSyntax else { return node }
32+
return newDecl.addModifier(modifierKeyword)
33+
}
34+
guard !hasAccessorKeyword(modifiers: modifiers) else { return node }
35+
let newModifiers = insertAccessorKeyword(curModifiers: modifiers)
36+
return node.withModifiers(newModifiers)
37+
}
38+
39+
override func visit(_ node: AssociatedtypeDeclSyntax) -> DeclSyntax {
40+
guard let modifiers = node.modifiers else {
41+
guard let newDecl = removeFirstTokLeadingTrivia(node: node)
42+
as? AssociatedtypeDeclSyntax else { return node }
43+
return newDecl.addModifier(modifierKeyword)
44+
}
45+
guard !hasAccessorKeyword(modifiers: modifiers) else { return node }
46+
let newModifiers = insertAccessorKeyword(curModifiers: modifiers)
47+
return node.withModifiers(newModifiers)
48+
}
49+
50+
override func visit(_ node: ClassDeclSyntax) -> DeclSyntax {
51+
guard let modifiers = node.modifiers else {
52+
guard let newDecl = removeFirstTokLeadingTrivia(node: node)
53+
as? ClassDeclSyntax else { return node }
54+
return newDecl.addModifier(modifierKeyword)
55+
}
56+
guard !hasAccessorKeyword(modifiers: modifiers) else { return node }
57+
let newModifiers = insertAccessorKeyword(curModifiers: modifiers)
58+
return node.withModifiers(newModifiers)
59+
}
60+
61+
override func visit(_ node: EnumDeclSyntax) -> DeclSyntax {
62+
guard let modifiers = node.modifiers else {
63+
guard let newDecl = removeFirstTokLeadingTrivia(node: node)
64+
as? EnumDeclSyntax else { return node }
65+
return newDecl.addModifier(modifierKeyword)
66+
}
67+
guard !hasAccessorKeyword(modifiers: modifiers) else { return node }
68+
let newModifiers = insertAccessorKeyword(curModifiers: modifiers)
69+
return node.withModifiers(newModifiers)
70+
}
71+
72+
override func visit(_ node: ProtocolDeclSyntax) -> DeclSyntax {
73+
guard let modifiers = node.modifiers else {
74+
guard let newDecl = removeFirstTokLeadingTrivia(node: node)
75+
as? ProtocolDeclSyntax else { return node }
76+
return newDecl.addModifier(modifierKeyword)
77+
}
78+
guard !hasAccessorKeyword(modifiers: modifiers) else { return node }
79+
let newModifiers = insertAccessorKeyword(curModifiers: modifiers)
80+
return node.withModifiers(newModifiers)
81+
}
82+
83+
override func visit(_ node: StructDeclSyntax) -> DeclSyntax {
84+
guard let modifiers = node.modifiers else {
85+
guard let newDecl = removeFirstTokLeadingTrivia(node: node)
86+
as? StructDeclSyntax else { return node }
87+
return newDecl.addModifier(modifierKeyword)
88+
}
89+
guard !hasAccessorKeyword(modifiers: modifiers) else { return node }
90+
let newModifiers = insertAccessorKeyword(curModifiers: modifiers)
91+
return node.withModifiers(newModifiers)
92+
}
93+
94+
override func visit(_ node: TypealiasDeclSyntax) -> DeclSyntax {
95+
guard let modifiers = node.modifiers else {
96+
guard let newDecl = removeFirstTokLeadingTrivia(node: node)
97+
as? TypealiasDeclSyntax else { return node }
98+
return newDecl.addModifier(modifierKeyword)
99+
}
100+
guard !hasAccessorKeyword(modifiers: modifiers) else { return node }
101+
let newModifiers = insertAccessorKeyword(curModifiers: modifiers)
102+
return node.withModifiers(newModifiers)
103+
}
104+
105+
override func visit(_ node: InitializerDeclSyntax) -> DeclSyntax {
106+
guard let modifiers = node.modifiers else {
107+
guard let newDecl = removeFirstTokLeadingTrivia(node: node)
108+
as? InitializerDeclSyntax else { return node }
109+
return newDecl.addModifier(modifierKeyword)
110+
}
111+
guard !hasAccessorKeyword(modifiers: modifiers) else { return node }
112+
let newModifiers = insertAccessorKeyword(curModifiers: modifiers)
113+
return node.withModifiers(newModifiers)
114+
}
115+
116+
117+
// Determines if declaration already has an access keyword in modifiers
118+
func hasAccessorKeyword(modifiers: ModifierListSyntax) -> Bool {
119+
for modifier in modifiers {
120+
let keywordKind = modifier.name.tokenKind
121+
switch keywordKind {
122+
case .publicKeyword, .privateKeyword, .fileprivateKeyword, .internalKeyword:
123+
return true
124+
default:
125+
continue
126+
}
127+
}
128+
return false
129+
}
130+
131+
// Puts the access keyword at the beginning of the given modifier list
132+
func insertAccessorKeyword(curModifiers: ModifierListSyntax) -> ModifierListSyntax {
133+
var newModifiers: [DeclModifierSyntax] = []
134+
newModifiers.append(contentsOf: curModifiers)
135+
newModifiers[0] = newModifiers[0].withName(newModifiers[0].name.withoutLeadingTrivia())
136+
newModifiers.insert(modifierKeyword, at: 0)
137+
return SyntaxFactory.makeModifierList(newModifiers)
138+
}
139+
140+
func removeFirstTokLeadingTrivia(node: DeclSyntax) -> DeclSyntax {
141+
let withoutLeadTrivia = replaceTrivia(on: node,
142+
token: node.firstToken,
143+
leadingTrivia: []) as! DeclSyntax
144+
return withoutLeadTrivia
145+
}
146+
}
147+
148+
func addModifier(declaration: MemberDeclListItemSyntax,
149+
modifierKeyword: DeclModifierSyntax) -> Syntax {
150+
return AddModifierRewriter(modifierKeyword: modifierKeyword).visit(declaration)
151+
}

Diff for: Sources/Rules/NoAccessLevelOnExtensionDeclaration.swift

+74
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,81 @@ import SwiftSyntax
1010
/// declaration in the extension; declarations with redundant access levels (e.g.
1111
/// `internal`, as that is the default access level) have the explicit access level removed.
1212
///
13+
/// TODO: Find a better way to access modifiers and keyword tokens besides casting each declaration
14+
///
1315
/// - SeeAlso: https://google.github.io/swift#access-levels
1416
public final class NoAccessLevelOnExtensionDeclaration: SyntaxFormatRule {
1517

18+
public override func visit(_ node: ExtensionDeclSyntax) -> DeclSyntax {
19+
guard let modifiers = node.modifiers, modifiers.count != 0 else { return node }
20+
21+
for accessKeyword in modifiers {
22+
23+
let keywordKind = accessKeyword.name.tokenKind
24+
switch keywordKind {
25+
// Public, private, or fileprivate keywords need to be moved to members
26+
case .publicKeyword, .privateKeyword, .fileprivateKeyword:
27+
diagnose(.moveAccessKeyword(keyword: accessKeyword.name.text), on: accessKeyword)
28+
let newMembers = SyntaxFactory.makeMemberDeclBlock(
29+
leftBrace: node.members.leftBrace,
30+
members: addMemberAccessKeywords(memDeclBlock: node.members, keyword: accessKeyword),
31+
rightBrace: node.members.rightBrace)
32+
return node.withMembers(newMembers)
33+
.withModifiers(removeModifier(curModifiers: modifiers, removal: accessKeyword))
34+
// Internal keyword redundant, delete
35+
case .internalKeyword:
36+
diagnose(.removeRedundantAccessKeyword(name: node.extendedType.description),
37+
on: accessKeyword)
38+
let newKeyword = replaceTrivia(on: node.extensionKeyword,
39+
token: node.extensionKeyword,
40+
leadingTrivia: accessKeyword.leadingTrivia) as! TokenSyntax
41+
return node.withModifiers(removeModifier(curModifiers: modifiers, removal: accessKeyword))
42+
.withExtensionKeyword(newKeyword)
43+
default:
44+
return node
45+
}
46+
}
47+
return node
48+
}
49+
50+
// Returns modifier list without the access modifier
51+
func removeModifier(curModifiers: ModifierListSyntax,
52+
removal: DeclModifierSyntax) -> ModifierListSyntax {
53+
var newMods: [DeclModifierSyntax] = []
54+
for modifier in curModifiers {
55+
if modifier.name != removal.name {
56+
newMods.append(modifier)
57+
}
58+
}
59+
return SyntaxFactory.makeModifierList(newMods)
60+
}
61+
62+
// Adds given keyword to all members in declaration block
63+
func addMemberAccessKeywords(memDeclBlock: MemberDeclBlockSyntax,
64+
keyword: DeclModifierSyntax) -> MemberDeclListSyntax {
65+
var newMembers: [MemberDeclListItemSyntax] = []
66+
67+
for member in memDeclBlock.members {
68+
guard let firstTokInDecl = member.firstToken else { continue }
69+
let formattedKeyword = replaceTrivia(on: keyword,
70+
token: keyword.name,
71+
leadingTrivia: firstTokInDecl.leadingTrivia)
72+
as! DeclModifierSyntax
73+
74+
guard let newMember = addModifier(declaration: member, modifierKeyword: formattedKeyword)
75+
as? MemberDeclListItemSyntax else { continue }
76+
newMembers.append(newMember)
77+
}
78+
return SyntaxFactory.makeMemberDeclList(newMembers)
79+
}
80+
}
81+
82+
extension Diagnostic.Message {
83+
static func removeRedundantAccessKeyword(name: String) -> Diagnostic.Message {
84+
return .init(.warning, "remove redundant 'internal' access keyword from \(name)")
85+
}
86+
87+
static func moveAccessKeyword(keyword: String) -> Diagnostic.Message {
88+
return .init(.warning, "specify \(keyword) access level for each member inside the extension")
89+
}
1690
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import Foundation
2+
import XCTest
3+
import SwiftSyntax
4+
5+
@testable import Rules
6+
7+
public class NoAccessLevelOnExtensionDeclarationTests: DiagnosingTestCase {
8+
public func testExtensionDeclarationAccessLevel() {
9+
XCTAssertFormatting(
10+
NoAccessLevelOnExtensionDeclaration.self,
11+
input: """
12+
public extension Foo {
13+
var x: Bool
14+
internal var y: Bool
15+
static var z: Bool
16+
static func someFunc() {}
17+
init() {}
18+
protocol SomeProtocol {}
19+
class SomeClass {}
20+
struct SomeStruct {}
21+
enum SomeEnum {}
22+
}
23+
internal extension Bar {
24+
var a: Int
25+
var b: Int
26+
}
27+
""",
28+
expected: """
29+
extension Foo {
30+
public var x: Bool
31+
internal var y: Bool
32+
public static var z: Bool
33+
public static func someFunc() {}
34+
public init() {}
35+
public protocol SomeProtocol {}
36+
public class SomeClass {}
37+
public struct SomeStruct {}
38+
public enum SomeEnum {}
39+
}
40+
extension Bar {
41+
var a: Int
42+
var b: Int
43+
}
44+
"""
45+
)
46+
}
47+
48+
#if !os(macOS)
49+
static let allTests = [
50+
NoAccessLevelOnExtensionDeclarationTests.testExtensionDeclarationAccessLevel,
51+
]
52+
#endif
53+
}

0 commit comments

Comments
 (0)