File: RuleCollector.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 (157 lines) | stat: -rw-r--r-- 6,036 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
//===----------------------------------------------------------------------===//
//
// 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 Foundation
import SwiftSyntax
import SwiftParser

@_spi(Rules) import SwiftFormat

/// Collects information about rules in the formatter code base.
final class RuleCollector {
  /// Information about a detected rule.
  struct DetectedRule: Hashable {
    /// The type name of the rule.
    let typeName: String

    /// The description of the rule, extracted from the rule class or struct DocC comment
    /// with `DocumentationCommentText(extractedFrom:)`
    let description: String?

    /// The syntax node types visited by the rule type.
    let visitedNodes: [String]

    /// Indicates whether the rule can format code (all rules can lint).
    let canFormat: Bool

    /// Indicates whether the rule is disabled by default, i.e. requires opting in to use it.
    let isOptIn: Bool
  }

  /// A list of all rules that can lint (thus also including format rules) found in the code base.
  var allLinters = Set<DetectedRule>()

  /// A list of all the format-only rules found in the code base.
  var allFormatters = Set<DetectedRule>()

  /// A dictionary mapping syntax node types to the lint/format rules that visit them.
  var syntaxNodeLinters = [String: [String]]()

  /// Populates the internal collections with rules in the given directory.
  ///
  /// - Parameter url: The file system URL that should be scanned for rules.
  func collect(from url: URL) throws {
    // For each file in the Rules directory, find types that either conform to SyntaxLintRule or
    // inherit from SyntaxFormatRule.
    let fm = FileManager.default
    guard let rulesEnumerator = fm.enumerator(atPath: url.path) else {
      fatalError("Could not list the directory \(url.path)")
    }

    for baseName in rulesEnumerator {
      // Ignore files that aren't Swift source files.
      guard let baseName = baseName as? String, baseName.hasSuffix(".swift") else { continue }

      let fileURL = url.appendingPathComponent(baseName)
      let fileInput = try String(contentsOf: fileURL)
      let sourceFile = Parser.parse(source: fileInput)

      for statement in sourceFile.statements {
        guard let detectedRule = self.detectedRule(at: statement) else { continue }

        if detectedRule.canFormat {
          // Format rules just get added to their own list; we run them each over the entire tree in
          // succession.
          allFormatters.insert(detectedRule)
        }

        // Lint rules (this includes format rules, which can also lint) get added to a mapping over
        // the names of the types they touch so that they can be interleaved into one pass over the
        // tree.
        allLinters.insert(detectedRule)
        for visitedNode in detectedRule.visitedNodes {
          syntaxNodeLinters[visitedNode, default: []].append(detectedRule.typeName)
        }
      }
    }
  }

  /// Determine the rule kind for the declaration in the given statement, if any.
  private func detectedRule(at statement: CodeBlockItemSyntax) -> DetectedRule? {
    let typeName: String
    let members: MemberBlockItemListSyntax
    let maybeInheritanceClause: InheritanceClauseSyntax?
    let description = DocumentationCommentText(extractedFrom: statement.item.leadingTrivia)

    if let classDecl = statement.item.as(ClassDeclSyntax.self) {
      typeName = classDecl.name.text
      members = classDecl.memberBlock.members
      maybeInheritanceClause = classDecl.inheritanceClause
    } else if let structDecl = statement.item.as(StructDeclSyntax.self) {
      typeName = structDecl.name.text
      members = structDecl.memberBlock.members
      maybeInheritanceClause = structDecl.inheritanceClause
    } else {
      return nil
    }

    // Make sure it has an inheritance clause.
    guard let inheritanceClause = maybeInheritanceClause else {
      return nil
    }

    // Scan through the inheritance clause to find one of the protocols/types we're interested in.
    for inheritance in inheritanceClause.inheritedTypes {
      guard let identifier = inheritance.type.as(IdentifierTypeSyntax.self) else {
        continue
      }

      let canFormat: Bool
      switch identifier.name.text {
      case "SyntaxLintRule":
        canFormat = false
      case "SyntaxFormatRule":
        canFormat = true
      default:
        // Keep looking at the other inheritances.
        continue
      }

      // Now that we know it's a format or lint rule, collect the `visit` methods.
      var visitedNodes = [String]()
      for member in members {
        guard let function = member.decl.as(FunctionDeclSyntax.self) else { continue }
        guard function.name.text == "visit" else { continue }
        let params = function.signature.parameterClause.parameters
        guard let firstType = params.firstAndOnly?.type.as(IdentifierTypeSyntax.self) else {
          continue
        }
        visitedNodes.append(firstType.name.text)
      }

      /// Ignore it if it doesn't have any; there's no point in putting no-op rules in the pipeline.
      /// Otherwise, return it (we don't need to look at the rest of the inheritances).
      guard !visitedNodes.isEmpty else { return nil }
      guard let ruleType = _typeByName("SwiftFormat.\(typeName)") as? Rule.Type else {
        preconditionFailure("Failed to find type for rule named \(typeName)")
      }
      return DetectedRule(
        typeName: typeName,
        description: description?.text,
        visitedNodes: visitedNodes,
        canFormat: canFormat,
        isOptIn: ruleType.isOptIn)
    }

    return nil
  }
}