Skip to content

Commit e052da5

Browse files
authored
Merge pull request #597 from allevato/fateful-findings
Fix a bunch of FIXMEs around linter findings.
2 parents 2dcf715 + 9e5ece1 commit e052da5

25 files changed

+413
-298
lines changed

Sources/SwiftFormat/PrettyPrint/WhitespaceLinter.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -481,7 +481,10 @@ extension Finding.Message {
481481

482482
public static let removeLineError: Finding.Message = "remove line break"
483483

484-
public static func addLinesError(_ lines: Int) -> Finding.Message { "add \(lines) line breaks" }
484+
public static func addLinesError(_ lines: Int) -> Finding.Message {
485+
let noun = lines == 1 ? "break" : "breaks"
486+
return "add \(lines) line \(noun)"
487+
}
485488

486489
public static let lineLengthError: Finding.Message = "line is too long"
487490
}

Sources/SwiftFormat/Rules/DoNotUseSemicolons.swift

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,10 +80,18 @@ public final class DoNotUseSemicolons: SyntaxFormatRule {
8080
}
8181
}
8282

83-
// This discards any trailingTrivia from the semicolon. That trivia is at most some spaces,
84-
// and the pretty printer adds any necessary spaces so it's safe to discard.
83+
// This discards any trailing trivia from the semicolon. That trivia will only be horizontal
84+
// whitespace, and the pretty printer adds any necessary spaces so it's safe to discard.
85+
// TODO: When we stop using the legacy trivia transform, we need to fix this to preserve
86+
// trailing comments.
8587
newItem = newItem.with(\.semicolon, nil)
86-
if idx < node.count - 1 {
88+
89+
// When emitting the finding, tell the user to move the next statement down if there is
90+
// another statement following this one. Otherwise, just tell them to remove the semicolon.
91+
if let nextToken = semicolon.nextToken(viewMode: .sourceAccurate),
92+
nextToken.tokenKind != .rightBrace && nextToken.tokenKind != .endOfFile
93+
&& !nextToken.leadingTrivia.containsNewlines
94+
{
8795
diagnose(.removeSemicolonAndMove, on: semicolon)
8896
} else {
8997
diagnose(.removeSemicolon, on: semicolon)

Sources/SwiftFormat/Rules/DontRepeatTypeInStaticProperties.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public final class DontRepeatTypeInStaticProperties: SyntaxLintRule {
7777
for pattern in varDecl.identifiers {
7878
let varName = pattern.identifier.text
7979
if varName.contains(bareTypeName) {
80-
diagnose(.removeTypeFromName(name: varName, type: bareTypeName), on: varDecl)
80+
diagnose(.removeTypeFromName(name: varName, type: bareTypeName), on: pattern.identifier)
8181
}
8282
}
8383
}

Sources/SwiftFormat/Rules/FullyIndirectEnum.swift

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,20 @@ public final class FullyIndirectEnum: SyntaxFormatRule {
2525
public override func visit(_ node: EnumDeclSyntax) -> DeclSyntax {
2626
let enumMembers = node.memberBlock.members
2727
guard !node.modifiers.has(modifier: "indirect"),
28-
allCasesAreIndirect(in: enumMembers)
28+
case let indirectModifiers = indirectModifiersIfAllCasesIndirect(in: enumMembers),
29+
!indirectModifiers.isEmpty
2930
else {
3031
return DeclSyntax(node)
3132
}
3233

33-
diagnose(.moveIndirectKeywordToEnumDecl(name: node.name.text), on: node.name)
34+
let notes = indirectModifiers.map { modifier in
35+
Finding.Note(
36+
message: .removeIndirect,
37+
location: Finding.Location(
38+
modifier.startLocation(converter: self.context.sourceLocationConverter)))
39+
}
40+
diagnose(
41+
.moveIndirectKeywordToEnumDecl(name: node.name.text), on: node.enumKeyword, notes: notes)
3442

3543
// Removes 'indirect' keyword from cases, reformats
3644
let newMembers = enumMembers.map {
@@ -75,17 +83,21 @@ public final class FullyIndirectEnum: SyntaxFormatRule {
7583
/// Returns a value indicating whether all enum cases in the given list are indirect.
7684
///
7785
/// Note that if the enum has no cases, this returns false.
78-
private func allCasesAreIndirect(in members: MemberBlockItemListSyntax) -> Bool {
79-
var hadCases = false
86+
private func indirectModifiersIfAllCasesIndirect(in members: MemberBlockItemListSyntax)
87+
-> [DeclModifierSyntax]
88+
{
89+
var indirectModifiers = [DeclModifierSyntax]()
8090
for member in members {
8191
if let caseMember = member.decl.as(EnumCaseDeclSyntax.self) {
82-
hadCases = true
83-
guard caseMember.modifiers.has(modifier: "indirect") else {
84-
return false
92+
guard let indirectModifier = caseMember.modifiers.first(
93+
where: { $0.name.text == "indirect" }
94+
) else {
95+
return []
8596
}
97+
indirectModifiers.append(indirectModifier)
8698
}
8799
}
88-
return hadCases
100+
return indirectModifiers
89101
}
90102

91103
/// Transfers given leading trivia to the first token in the case declaration.
@@ -112,6 +124,8 @@ public final class FullyIndirectEnum: SyntaxFormatRule {
112124
extension Finding.Message {
113125
@_spi(Rules)
114126
public static func moveIndirectKeywordToEnumDecl(name: String) -> Finding.Message {
115-
"move 'indirect' before the enum declaration '\(name)' when all cases are indirect"
127+
"declare enum '\(name)' itself as indirect when all cases are indirect"
116128
}
129+
130+
public static let removeIndirect: Finding.Message = "remove 'indirect' here"
117131
}

Sources/SwiftFormat/Rules/GroupNumericLiterals.swift

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,21 +40,21 @@ public final class GroupNumericLiterals: SyntaxFormatRule {
4040
// Hexadecimal
4141
let digitsNoPrefix = String(originalDigits.dropFirst(2))
4242
guard digitsNoPrefix.count >= 8 else { return ExprSyntax(node) }
43-
diagnose(.groupNumericLiteral(every: 4), on: node)
43+
diagnose(.groupNumericLiteral(every: 4, base: "hexadecimal"), on: node)
4444
newDigits = "0x" + digits(digitsNoPrefix, groupedEvery: 4)
4545
case "0b":
4646
// Binary
4747
let digitsNoPrefix = String(originalDigits.dropFirst(2))
4848
guard digitsNoPrefix.count >= 10 else { return ExprSyntax(node) }
49-
diagnose(.groupNumericLiteral(every: 8), on: node)
49+
diagnose(.groupNumericLiteral(every: 8, base: "binary"), on: node)
5050
newDigits = "0b" + digits(digitsNoPrefix, groupedEvery: 8)
5151
case "0o":
5252
// Octal
5353
return ExprSyntax(node)
5454
default:
5555
// Decimal
5656
guard originalDigits.count >= 7 else { return ExprSyntax(node) }
57-
diagnose(.groupNumericLiteral(every: 3), on: node)
57+
diagnose(.groupNumericLiteral(every: 3, base: "decimal"), on: node)
5858
newDigits = digits(originalDigits, groupedEvery: 3)
5959
}
6060

@@ -84,8 +84,7 @@ public final class GroupNumericLiterals: SyntaxFormatRule {
8484

8585
extension Finding.Message {
8686
@_spi(Rules)
87-
public static func groupNumericLiteral(every stride: Int) -> Finding.Message {
88-
let ending = stride == 3 ? "rd" : "th"
89-
return "group numeric literal using '_' every \(stride)\(ending) number"
87+
public static func groupNumericLiteral(every stride: Int, base: String) -> Finding.Message {
88+
return "group every \(stride) digits in this \(base) literal using a '_' separator"
9089
}
9190
}

Sources/SwiftFormat/Rules/NoAccessLevelOnExtensionDeclaration.swift

Lines changed: 39 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,8 @@ import SwiftSyntax
1919
/// Format: The access level is removed from the extension declaration and is added to each
2020
/// declaration in the extension; declarations with redundant access levels (e.g.
2121
/// `internal`, as that is the default access level) have the explicit access level removed.
22-
///
23-
/// TODO: Find a better way to access modifiers and keyword tokens besides casting each declaration
2422
@_spi(Rules)
2523
public final class NoAccessLevelOnExtensionDeclaration: SyntaxFormatRule {
26-
2724
public override func visit(_ node: ExtensionDeclSyntax) -> DeclSyntax {
2825
guard !node.modifiers.isEmpty else { return DeclSyntax(node) }
2926
guard let accessKeyword = node.modifiers.accessLevelModifier else { return DeclSyntax(node) }
@@ -32,21 +29,26 @@ public final class NoAccessLevelOnExtensionDeclaration: SyntaxFormatRule {
3229
switch keywordKind {
3330
// Public, private, or fileprivate keywords need to be moved to members
3431
case .keyword(.public), .keyword(.private), .keyword(.fileprivate):
35-
diagnose(.moveAccessKeyword(keyword: accessKeyword.name.text), on: accessKeyword)
36-
3732
// The effective access level of the members of a `private` extension is `fileprivate`, so
3833
// we have to update the keyword to ensure that the result is correct.
3934
let accessKeywordToAdd: DeclModifierSyntax
35+
let message: Finding.Message
4036
if keywordKind == .keyword(.private) {
4137
accessKeywordToAdd
4238
= accessKeyword.with(\.name, accessKeyword.name.with(\.tokenKind, .keyword(.fileprivate)))
39+
message = .moveAccessKeywordAndMakeFileprivate(keyword: accessKeyword.name.text)
4340
} else {
4441
accessKeywordToAdd = accessKeyword
42+
message = .moveAccessKeyword(keyword: accessKeyword.name.text)
4543
}
4644

45+
let (newMemberBlock, notes) = addMemberAccessKeywords(
46+
memDeclBlock: node.memberBlock, keyword: accessKeywordToAdd)
47+
diagnose(message, on: accessKeyword, notes: notes)
48+
4749
let newMembers = MemberBlockSyntax(
4850
leftBrace: node.memberBlock.leftBrace,
49-
members: addMemberAccessKeywords(memDeclBlock: node.memberBlock, keyword: accessKeywordToAdd),
51+
members: newMemberBlock,
5052
rightBrace: node.memberBlock.rightBrace)
5153
var newKeyword = node.extensionKeyword
5254
newKeyword.leadingTrivia = accessKeyword.leadingTrivia
@@ -57,9 +59,7 @@ public final class NoAccessLevelOnExtensionDeclaration: SyntaxFormatRule {
5759

5860
// Internal keyword redundant, delete
5961
case .keyword(.internal):
60-
diagnose(
61-
.removeRedundantAccessKeyword(name: node.extendedType.description),
62-
on: accessKeyword)
62+
diagnose(.removeRedundantAccessKeyword, on: accessKeyword)
6363
var newKeyword = node.extensionKeyword
6464
newKeyword.leadingTrivia = accessKeyword.leadingTrivia
6565
let result = node.with(\.modifiers, node.modifiers.remove(name: accessKeyword.name.text))
@@ -76,33 +76,56 @@ public final class NoAccessLevelOnExtensionDeclaration: SyntaxFormatRule {
7676
private func addMemberAccessKeywords(
7777
memDeclBlock: MemberBlockSyntax,
7878
keyword: DeclModifierSyntax
79-
) -> MemberBlockItemListSyntax {
79+
) -> (MemberBlockItemListSyntax, [Finding.Note]) {
8080
var newMembers: [MemberBlockItemSyntax] = []
81+
var notes: [Finding.Note] = []
8182

8283
var formattedKeyword = keyword
8384
formattedKeyword.leadingTrivia = []
8485

8586
for memberItem in memDeclBlock.members {
8687
let member = memberItem.decl
8788
guard
89+
let modifiers = member.asProtocol(WithModifiersSyntax.self)?.modifiers,
8890
// addModifier relocates trivia for any token(s) displaced by the new modifier.
8991
let newDecl = addModifier(declaration: member, modifierKeyword: formattedKeyword)
9092
.as(DeclSyntax.self)
91-
else { continue }
93+
else {
94+
newMembers.append(memberItem)
95+
continue
96+
}
97+
9298
newMembers.append(memberItem.with(\.decl, newDecl))
99+
100+
// If it already had an explicit access modifier, don't leave a note.
101+
if modifiers.accessLevelModifier == nil {
102+
notes.append(Finding.Note(
103+
message: .addModifierToExtensionMember(keyword: formattedKeyword.name.text),
104+
location: Finding.Location(member.startLocation(converter: context.sourceLocationConverter))
105+
))
106+
}
93107
}
94-
return MemberBlockItemListSyntax(newMembers)
108+
return (MemberBlockItemListSyntax(newMembers), notes)
95109
}
96110
}
97111

98112
extension Finding.Message {
99113
@_spi(Rules)
100-
public static func removeRedundantAccessKeyword(name: String) -> Finding.Message {
101-
"remove redundant 'internal' access keyword from '\(name)'"
102-
}
114+
public static let removeRedundantAccessKeyword: Finding.Message =
115+
"remove this redundant 'internal' access modifier from this extension"
103116

104117
@_spi(Rules)
105118
public static func moveAccessKeyword(keyword: String) -> Finding.Message {
106-
"move the '\(keyword)' access keyword to precede each member inside the extension"
119+
"move this '\(keyword)' access modifier to precede each member inside this extension"
120+
}
121+
122+
@_spi(Rules)
123+
public static func moveAccessKeywordAndMakeFileprivate(keyword: String) -> Finding.Message {
124+
"remove this '\(keyword)' access modifier and declare each member inside this extension as 'fileprivate'"
125+
}
126+
127+
@_spi(Rules)
128+
public static func addModifierToExtensionMember(keyword: String) -> Finding.Message {
129+
"add '\(keyword)' access modifier to this declaration"
107130
}
108131
}

Sources/SwiftFormat/Rules/NoCasesWithOnlyFallthrough.swift

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public final class NoCasesWithOnlyFallthrough: SyntaxFormatRule {
2828
/// Flushes any un-collapsed violations to the new cases list.
2929
func flushViolations() {
3030
fallthroughOnlyCases.forEach {
31-
newChildren.append(.switchCase(super.visit($0)))
31+
newChildren.append(.switchCase(visit($0)))
3232
}
3333
fallthroughOnlyCases.removeAll()
3434
}
@@ -57,7 +57,8 @@ public final class NoCasesWithOnlyFallthrough: SyntaxFormatRule {
5757
if canMergeWithPreviousCases(switchCase) {
5858
// If the current case can be merged with the ones before it, merge them all, leaving no
5959
// `fallthrough`-only cases behind.
60-
newChildren.append(.switchCase(visit(mergedCases(fallthroughOnlyCases + [switchCase]))))
60+
let newSwitchCase = visit(switchCase)
61+
newChildren.append(.switchCase(visit(mergedCases(fallthroughOnlyCases + [newSwitchCase]))))
6162
} else {
6263
// If the current case can't be merged with the ones before it, merge the previous ones
6364
// into a single `fallthrough`-only case and then append the current one. This could
@@ -171,6 +172,11 @@ public final class NoCasesWithOnlyFallthrough: SyntaxFormatRule {
171172
var newCaseItems: [SwitchCaseItemSyntax] = []
172173
let labels = cases.lazy.compactMap({ $0.label.as(SwitchCaseLabelSyntax.self) })
173174
for label in labels.dropLast() {
175+
// Diagnose the cases being collapsed. We do this for all but the last one in the array; the
176+
// last one isn't diagnosed because it will contain the body that applies to all the previous
177+
// cases.
178+
diagnose(.collapseCase, on: label)
179+
174180
// We can blindly append all but the last case item because they must already have a trailing
175181
// comma. Then, we need to add a trailing comma to the last one, since it will be followed by
176182
// more items.
@@ -179,11 +185,6 @@ public final class NoCasesWithOnlyFallthrough: SyntaxFormatRule {
179185
label.caseItems.last!.with(
180186
\.trailingComma,
181187
TokenSyntax.commaToken(trailingTrivia: .spaces(1))))
182-
183-
// Diagnose the cases being collapsed. We do this for all but the last one in the array; the
184-
// last one isn't diagnosed because it will contain the body that applies to all the previous
185-
// cases.
186-
diagnose(.collapseCase, on: label)
187188
}
188189
newCaseItems.append(contentsOf: labels.last!.caseItems)
189190

Sources/SwiftFormat/Rules/NoEmptyTrailingClosureParentheses.swift

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,18 @@ public final class NoEmptyTrailingClosureParentheses: SyntaxFormatRule {
2424
public override func visit(_ node: FunctionCallExprSyntax) -> ExprSyntax {
2525
guard node.arguments.count == 0 else { return super.visit(node) }
2626

27-
guard let trailingClosure = node.trailingClosure,
28-
node.arguments.isEmpty && node.leftParen != nil else
29-
{
27+
guard
28+
let trailingClosure = node.trailingClosure,
29+
let leftParen = node.leftParen,
30+
node.arguments.isEmpty
31+
else {
3032
return super.visit(node)
3133
}
3234
guard let name = node.calledExpression.lastToken(viewMode: .sourceAccurate) else {
3335
return super.visit(node)
3436
}
3537

36-
diagnose(.removeEmptyTrailingParentheses(name: "\(name.trimmedDescription)"), on: node)
38+
diagnose(.removeEmptyTrailingParentheses(name: "\(name.trimmedDescription)"), on: leftParen)
3739

3840
// Need to visit `calledExpression` before creating a new node so that the location data (column
3941
// and line numbers) is available.

Sources/SwiftFormat/Rules/NoParensAroundConditions.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ public final class NoParensAroundConditions: SyntaxFormatRule {
4242
}
4343
}
4444

45-
diagnose(.removeParensAroundExpression, on: expr)
45+
diagnose(.removeParensAroundExpression, on: tuple.leftParen)
4646

4747
guard
4848
let visitedTuple = visit(tuple).as(TupleExprSyntax.self),
@@ -75,7 +75,6 @@ public final class NoParensAroundConditions: SyntaxFormatRule {
7575
return node.with(\.condition, .expression(extractExpr(tup)))
7676
}
7777

78-
/// FIXME(hbh): Parsing for SwitchExprSyntax is not implemented.
7978
public override func visit(_ node: SwitchExprSyntax) -> ExprSyntax {
8079
guard let tup = node.subject.as(TupleExprSyntax.self),
8180
tup.elements.firstAndOnly != nil

0 commit comments

Comments
 (0)