File: DoNotUseSemicolons.swift

package info (click to toggle)
swiftlang 6.0.3-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 2,519,992 kB
  • sloc: cpp: 9,107,863; ansic: 2,040,022; asm: 1,135,751; python: 296,500; objc: 82,456; f90: 60,502; lisp: 34,951; pascal: 19,946; sh: 18,133; perl: 7,482; ml: 4,937; javascript: 4,117; makefile: 3,840; awk: 3,535; xml: 914; fortran: 619; cs: 573; ruby: 573
file content (129 lines) | stat: -rw-r--r-- 5,277 bytes parent folder | download
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"
}