1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129
|
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2019 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 SwiftSyntax
/// Semicolons should not be present in Swift code.
///
/// Lint: If a semicolon appears anywhere, a lint error is raised.
///
/// Format: All semicolons will be replaced with line breaks.
@_spi(Rules)
public final class DoNotUseSemicolons: SyntaxFormatRule {
/// Creates a new version of the given node which doesn't contain any semicolons. The node's
/// items are recursively modified to remove semicolons, replacing with line breaks where needed.
/// Items are checked recursively to support items that contain code blocks, which may have
/// semicolons to be removed.
///
/// - Parameters:
/// - node: A node that contains items which may have semicolons or nested code blocks.
/// - nodeCreator: A closure that creates a new node given an array of items.
private func nodeByRemovingSemicolons<
ItemType: SyntaxProtocol & WithSemicolonSyntax & Equatable,
NodeType: SyntaxCollection
>(from node: NodeType) -> NodeType where NodeType.Element == ItemType {
var newItems = Array(node)
// Keeps track of trailing trivia after a semicolon when it needs to be moved to precede the
// next statement.
var pendingTrivia = Trivia()
for (idx, item) in node.enumerated() {
// Check for semicolons in statements inside of the item, because code blocks may be nested
// inside of other code blocks.
guard var newItem = rewrite(Syntax(item)).as(ItemType.self) else {
return node
}
// Check if we need to make any modifications (removing semicolon/adding newlines).
guard newItem != item || item.semicolon != nil || !pendingTrivia.isEmpty else {
continue
}
// Check if the leading trivia for this statement needs a new line.
if !pendingTrivia.isEmpty {
newItem.leadingTrivia = pendingTrivia + newItem.leadingTrivia
}
pendingTrivia = []
// If there's a semicolon, diagnose and remove it.
// Exception: Do not remove the semicolon if it is separating a `do` statement from a `while`
// statement.
if let semicolon = item.semicolon,
!(idx < node.count - 1
&& isCodeBlockItem(item, containingStmtType: DoStmtSyntax.self)
&& isCodeBlockItem(newItems[idx + 1], containingStmtType: WhileStmtSyntax.self))
{
// When emitting the finding, tell the user to move the next statement down if there is
// another statement following this one. Otherwise, just tell them to remove the semicolon.
var hasNextStatement: Bool
if let nextToken = semicolon.nextToken(viewMode: .sourceAccurate),
nextToken.tokenKind != .rightBrace && nextToken.tokenKind != .endOfFile
&& !nextToken.leadingTrivia.containsNewlines
{
hasNextStatement = true
pendingTrivia = [.newlines(1)]
diagnose(.removeSemicolonAndMove, on: semicolon)
} else {
hasNextStatement = false
diagnose(.removeSemicolon, on: semicolon)
}
// We treat block comments after the semicolon slightly differently from end-of-line
// comments. Assume that an end-of-line comment should stay on the same line when a
// semicolon is removed, but if we have something like `f(); /* blah */ g()`, assume that
// the comment is meant to be associated with `g()` (because it's not separated from that
// statement).
let trailingTrivia = newItem.trailingTrivia
newItem.semicolon = nil
if trailingTrivia.hasLineComment || !hasNextStatement {
newItem.trailingTrivia = trailingTrivia
} else {
pendingTrivia += trailingTrivia.withoutLeadingSpaces()
}
}
newItems[idx] = newItem
}
return NodeType(newItems)
}
public override func visit(_ node: CodeBlockItemListSyntax) -> CodeBlockItemListSyntax {
return nodeByRemovingSemicolons(from: node)
}
public override func visit(_ node: MemberBlockItemListSyntax) -> MemberBlockItemListSyntax {
return nodeByRemovingSemicolons(from: node)
}
/// Returns true if the given syntax node is a `CodeBlockItem` containing a statement node of the
/// given type.
private func isCodeBlockItem(
_ node: some SyntaxProtocol,
containingStmtType stmtType: StmtSyntaxProtocol.Type
) -> Bool {
if let codeBlockItem = node.as(CodeBlockItemSyntax.self),
case .stmt(let stmt) = codeBlockItem.item,
stmt.is(stmtType)
{
return true
}
return false
}
}
extension Finding.Message {
fileprivate static let removeSemicolon: Finding.Message = "remove ';'"
fileprivate static let removeSemicolonAndMove: Finding.Message =
"remove ';' and move the next statement to a new line"
}
|