File: UseWhereClausesInForLoops.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 (130 lines) | stat: -rw-r--r-- 4,778 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
130
//===----------------------------------------------------------------------===//
//
// 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

/// `for` loops that consist of a single `if` statement must use `where` clauses instead.
///
/// Lint: `for` loops that consist of a single `if` statement yield a lint error.
///
/// Format: `for` loops that consist of a single `if` statement have the conditional of that
///         statement factored out to a `where` clause.
@_spi(Rules)
public final class UseWhereClausesInForLoops: SyntaxFormatRule {

  /// Identifies this rule as being opt-in. This rule is experimental and not yet stable enough to
  /// be enabled by default.
  public override class var isOptIn: Bool { return true }

  public override func visit(_ node: ForStmtSyntax) -> StmtSyntax {
    // Extract IfStmt node if it's the only node in the function's body.
    guard !node.body.statements.isEmpty else { return StmtSyntax(node) }
    let firstStatement = node.body.statements.first!

    // Ignore for-loops with a `where` clause already.
    // TODO: Create an `&&` expression with both conditions?
    guard node.whereClause == nil else { return StmtSyntax(node) }

    // Match:
    //  - If the for loop has 1 statement, and it is an IfStmt, with a single
    //    condition.
    //  - If the for loop has 1 or more statement, and the first is a GuardStmt
    //    with a single condition whose body is just `continue`.
    switch firstStatement.item {
    case .stmt(let statement):
      return StmtSyntax(diagnoseAndUpdateForInStatement(firstStmt: statement, forInStmt: node))
    default:
      return StmtSyntax(node)
    }
  }

  private func diagnoseAndUpdateForInStatement(
    firstStmt: StmtSyntax,
    forInStmt: ForStmtSyntax
  ) -> ForStmtSyntax {
    switch Syntax(firstStmt).as(SyntaxEnum.self) {
    case .expressionStmt(let exprStmt):
      switch Syntax(exprStmt.expression).as(SyntaxEnum.self) {
      case .ifExpr(let ifExpr)
        where ifExpr.conditions.count == 1
        && ifExpr.elseKeyword == nil
        && forInStmt.body.statements.count == 1:
        // Extract the condition of the IfExpr.
        let conditionElement = ifExpr.conditions.first!
        guard let condition = conditionElement.condition.as(ExprSyntax.self) else {
          return forInStmt
        }
        diagnose(.useWhereInsteadOfIf, on: ifExpr)
        return updateWithWhereCondition(
          node: forInStmt,
          condition: condition,
          statements: ifExpr.body.statements
        )
      default:
        return forInStmt
      }
    case .guardStmt(let guardStmt)
    where guardStmt.conditions.count == 1
      && guardStmt.body.statements.count == 1
      && guardStmt.body.statements.first!.item.is(ContinueStmtSyntax.self):
      // Extract the condition of the GuardStmt.
      let conditionElement = guardStmt.conditions.first!
      guard let condition = conditionElement.condition.as(ExprSyntax.self) else {
        return forInStmt
      }
      diagnose(.useWhereInsteadOfGuard, on: guardStmt)
      return updateWithWhereCondition(
        node: forInStmt,
        condition: condition,
        statements: CodeBlockItemListSyntax(forInStmt.body.statements.dropFirst())
      )

    default:
      return forInStmt
    }
  }
}

fileprivate func updateWithWhereCondition(
  node: ForStmtSyntax,
  condition: ExprSyntax,
  statements: CodeBlockItemListSyntax
) -> ForStmtSyntax {
  // Construct a new `where` clause with the condition.
  let lastToken = node.sequence.lastToken(viewMode: .sourceAccurate)
  var whereLeadingTrivia = Trivia()
  if lastToken?.trailingTrivia.containsSpaces == false {
    whereLeadingTrivia = .spaces(1)
  }
  let whereKeyword = TokenSyntax.keyword(.where,
    leadingTrivia: whereLeadingTrivia,
    trailingTrivia: .spaces(1)
  )
  let whereClause = WhereClauseSyntax(
    whereKeyword: whereKeyword,
    condition: condition
  )

  // Replace the where clause and extract the body from the IfStmt.
  var result = node
  result.whereClause = whereClause
  result.body.statements = statements
  return result
}

extension Finding.Message {
  fileprivate static let useWhereInsteadOfIf: Finding.Message =
    "replace this 'if' statement with a 'where' clause"

  fileprivate static let useWhereInsteadOfGuard: Finding.Message =
    "replace this 'guard' statement with a 'where' clause"
}