File: NoAssignmentInExpressions.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 (166 lines) | stat: -rw-r--r-- 6,878 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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
//===----------------------------------------------------------------------===//
//
// 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

/// Assignment expressions must be their own statements.
///
/// Assignment should not be used in an expression context that expects a `Void` value. For example,
/// assigning a variable within a `return` statement existing a `Void` function is prohibited.
///
/// Lint: If an assignment expression is found in a position other than a standalone statement, a
///       lint finding is emitted.
///
/// Format: A `return` statement containing an assignment expression is expanded into two separate
///         statements.
@_spi(Rules)
public final class NoAssignmentInExpressions: SyntaxFormatRule {
  public override func visit(_ node: InfixOperatorExprSyntax) -> ExprSyntax {
    // Diagnose any assignment that isn't directly a child of a `CodeBlockItem` (which would be the
    // case if it was its own statement).
    if isAssignmentExpression(node)
      && !isStandaloneAssignmentStatement(node)
      && !isInAllowedFunction(node)
    {
      diagnose(.moveAssignmentToOwnStatement, on: node)
    }
    return ExprSyntax(node)
  }

  public override func visit(_ node: CodeBlockItemListSyntax) -> CodeBlockItemListSyntax {
    var newItems = [CodeBlockItemSyntax]()
    newItems.reserveCapacity(node.count)

    for item in node {
      // Make sure to visit recursively so that any nested decls get processed first.
      let visitedItem = visit(item)

      // Rewrite any `return <assignment>` expressions as `<assignment><newline>return`.
      switch visitedItem.item {
      case .stmt(let stmt):
        guard
          var returnStmt = stmt.as(ReturnStmtSyntax.self),
          let assignmentExpr = assignmentExpression(from: returnStmt)
        else {
          // Head to the default case where we just keep the original item.
          fallthrough
        }

        // Move the leading trivia from the `return` statement to the new assignment statement,
        // since that's a more sensible place than between the two.
        var assignmentItem = CodeBlockItemSyntax(item: .expr(ExprSyntax(assignmentExpr)))
        assignmentItem.leadingTrivia =
          returnStmt.leadingTrivia
            + returnStmt.returnKeyword.trailingTrivia.withoutLeadingSpaces()
            + assignmentExpr.leadingTrivia
        assignmentItem.trailingTrivia = []

        let trailingTrivia = returnStmt.trailingTrivia
        returnStmt.expression = nil
        returnStmt.returnKeyword.trailingTrivia = []
        var returnItem = CodeBlockItemSyntax(item: .stmt(StmtSyntax(returnStmt)))
        returnItem.leadingTrivia = [.newlines(1)]
        returnItem.trailingTrivia = trailingTrivia

        newItems.append(assignmentItem)
        newItems.append(returnItem)

      default:
        newItems.append(visitedItem)
      }
    }

    return CodeBlockItemListSyntax(newItems)
  }

  /// Extracts and returns the assignment expression in the given `return` statement, if there was
  /// one.
  ///
  /// If the `return` statement did not have an expression or if its expression was not an
  /// assignment expression, nil is returned.
  private func assignmentExpression(from returnStmt: ReturnStmtSyntax) -> InfixOperatorExprSyntax? {
    guard
      let returnExpr = returnStmt.expression,
      let infixOperatorExpr = returnExpr.as(InfixOperatorExprSyntax.self)
    else {
      return nil
    }
    return isAssignmentExpression(infixOperatorExpr) ? infixOperatorExpr : nil
  }

  /// Returns a value indicating whether the given infix operator expression is an assignment
  /// expression (either simple assignment with `=` or compound assignment with an operator like
  /// `+=`).
  private func isAssignmentExpression(_ expr: InfixOperatorExprSyntax) -> Bool {
    if expr.operator.is(AssignmentExprSyntax.self) {
      return true
    }
    guard let binaryOp = expr.operator.as(BinaryOperatorExprSyntax.self) else {
      return false
    }
    return context.operatorTable.infixOperator(named: binaryOp.operator.text)?.precedenceGroup
      == "AssignmentPrecedence"
  }

  /// Returns a value indicating whether the given node is a standalone assignment statement.
  ///
  /// This function considers try/await expressions and automatically walks up through them as
  /// needed. This is because `try f().x = y` should still be a standalone assignment for our
  /// purposes, even though a `TryExpr` will wrap the `InfixOperatorExpr` and thus would not be
  /// considered a standalone assignment if we only checked the infix expression for a
  /// `CodeBlockItem` parent.
  private func isStandaloneAssignmentStatement(_ node: InfixOperatorExprSyntax) -> Bool {
    var node = Syntax(node)
    while
      let parent = node.parent,
      parent.is(TryExprSyntax.self) || parent.is(AwaitExprSyntax.self)
    {
      node = parent
    }

    guard let parent = node.parent else {
      // This shouldn't happen under normal circumstances (i.e., unless the expression is detached
      // from the rest of a tree). In that case, we may as well consider it to be "standalone".
      return true
    }
    return parent.is(CodeBlockItemSyntax.self)
  }

  /// Returns true if the infix operator expression is in the (non-closure) parameters of an allowed
  /// function call.
  private func isInAllowedFunction(_ node: InfixOperatorExprSyntax) -> Bool {
    let allowedFunctions = context.configuration.noAssignmentInExpressions.allowedFunctions
    // Walk up the tree until we find a FunctionCallExprSyntax, and if the name matches, return
    // true. However, stop early if we hit a CodeBlockItemSyntax first; this would represent a
    // closure context where we *don't* want the exception to apply (for example, in
    // `someAllowedFunction(a, b) { return c = d }`, the `c = d` is a descendent of a function call
    // but we want it to be evaluated in its own context.
    var node = Syntax(node)
    while let parent = node.parent {
      node = parent
      if node.is(CodeBlockItemSyntax.self) {
        break
      }
      if let functionCallExpr = node.as(FunctionCallExprSyntax.self),
        allowedFunctions.contains(functionCallExpr.calledExpression.trimmedDescription)
      {
        return true
      }
    }
    return false
  }
}

extension Finding.Message {
  fileprivate static let moveAssignmentToOwnStatement: Finding.Message =
    "move this assignment expression into its own statement"
}