File: LintOrFormatRuleTestCase.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 (195 lines) | stat: -rw-r--r-- 7,986 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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
import SwiftFormat
import SwiftOperators
import SwiftParser
import SwiftSyntax
import XCTest

@_spi(Rules) @_spi(Testing) import SwiftFormat
@_spi(Testing) import _SwiftFormatTestSupport

class LintOrFormatRuleTestCase: DiagnosingTestCase {
  /// Performs a lint using the provided linter rule on the provided input and asserts that the
  /// emitted findings are correct.
  ///
  /// - Parameters:
  ///   - type: The metatype of the lint rule you wish to perform.
  ///   - markedSource: The input source code, which may include emoji markers at the locations
  ///     where findings are expected to be emitted.
  ///   - findings: A list of `FindingSpec` values that describe the findings that are expected to
  ///     be emitted.
  ///   - file: The file the test resides in (defaults to the current caller's file).
  ///   - line: The line the test resides in (defaults to the current caller's line).
  final func assertLint<LintRule: SyntaxLintRule>(
    _ type: LintRule.Type,
    _ markedSource: String,
    findings: [FindingSpec] = [],
    file: StaticString = #file,
    line: UInt = #line
  ) {
    let markedText = MarkedText(textWithMarkers: markedSource)
    let unmarkedSource = markedText.textWithoutMarkers
    let tree = Parser.parse(source: unmarkedSource)
    let sourceFileSyntax =
      try! OperatorTable.standardOperators.foldAll(tree).as(SourceFileSyntax.self)!

    var emittedFindings = [Finding]()

    // Force the rule to be enabled while we test it.
    var configuration = Configuration.forTesting
    configuration.rules[type.ruleName] = true
    let context = makeContext(
      sourceFileSyntax: sourceFileSyntax,
      configuration: configuration,
      selection: .infinite,
      findingConsumer: { emittedFindings.append($0) })
    let linter = type.init(context: context)
    linter.walk(sourceFileSyntax)

    assertFindings(
      expected: findings,
      markerLocations: markedText.markers,
      emittedFindings: emittedFindings,
      context: context,
      file: file,
      line: line)

    var emittedPipelineFindings = [Finding]()
    // Disable default rules, so only select rule runs in pipeline
    configuration.rules = [type.ruleName: true]
    let pipeline = SwiftLinter(
      configuration: configuration,
      findingConsumer: { emittedPipelineFindings.append($0) })
    pipeline.debugOptions.insert(.disablePrettyPrint)
    try! pipeline.lint(
      syntax: sourceFileSyntax,
      source: unmarkedSource,
      operatorTable: OperatorTable.standardOperators,
      assumingFileURL: URL(string: file.description)!)

    // Check that pipeline produces the same findings as the isolated linter rule
    assertFindings(
      expected: findings,
      markerLocations: markedText.markers,
      emittedFindings: emittedPipelineFindings,
      context: context,
      file: file,
      line: line)
  }

  /// Asserts that the result of applying a formatter to the provided input code yields the output.
  ///
  /// This method should be called by each test of each rule.
  ///
  /// - Parameters:
  ///   - formatType: The metatype of the format rule you wish to apply.
  ///   - input: The unformatted input code.
  ///   - expected: The expected result of formatting the input code.
  ///   - findings: A list of `FindingSpec` values that describe the findings that are expected to
  ///     be emitted.
  ///   - configuration: The configuration to use when formatting (or nil to use the default).
  ///   - file: The file the test resides in (defaults to the current caller's file)
  ///   - line:  The line the test resides in (defaults to the current caller's line)
  final func assertFormatting(
    _ formatType: SyntaxFormatRule.Type,
    input: String,
    expected: String,
    findings: [FindingSpec] = [],
    configuration: Configuration? = nil,
    file: StaticString = #file,
    line: UInt = #line
  ) {
    let markedInput = MarkedText(textWithMarkers: input)
    let originalSource: String = markedInput.textWithoutMarkers
    let tree = Parser.parse(source: originalSource)
    let sourceFileSyntax =
      try! OperatorTable.standardOperators.foldAll(tree).as(SourceFileSyntax.self)!

    var emittedFindings = [Finding]()

    // Force the rule to be enabled while we test it.
    var configuration = configuration ?? Configuration.forTesting
    configuration.rules[formatType.ruleName] = true
    let context = makeContext(
      sourceFileSyntax: sourceFileSyntax,
      configuration: configuration,
      selection: .infinite,
      findingConsumer: { emittedFindings.append($0) })

    let formatter = formatType.init(context: context)
    let actual = formatter.visit(sourceFileSyntax)
    assertStringsEqualWithDiff("\(actual)", expected, file: file, line: line)

    assertFindings(
      expected: findings,
      markerLocations: markedInput.markers,
      emittedFindings: emittedFindings,
      context: context,
      file: file,
      line: line)

    // Verify that the pretty printer can consume the transformed tree (e.g., it does not contain
    // any unfolded `SequenceExpr`s). Then do a whitespace-insensitive comparison of the two trees
    // to verify that the format rule didn't transform the tree in such a way that it caused the
    // pretty-printer to drop important information (the most likely case is a format rule
    // misplacing trivia in a way that the pretty-printer isn't able to handle).
    let prettyPrintedSource = PrettyPrinter(
      context: context,
      source: originalSource,
      node: Syntax(actual),
      printTokenStream: false,
      whitespaceOnly: false
    ).prettyPrint()
    let prettyPrintedTree = Parser.parse(source: prettyPrintedSource)
    XCTAssertEqual(
      whitespaceInsensitiveText(of: actual),
      whitespaceInsensitiveText(of: prettyPrintedTree),
      "After pretty-printing and removing fluid whitespace, the files did not match",
      file: file, line: line)

    var emittedPipelineFindings = [Finding]()
    // Disable default rules, so only select rule runs in pipeline
    configuration.rules = [formatType.ruleName: true]
    let pipeline = SwiftFormatter(
      configuration: configuration, findingConsumer: { emittedPipelineFindings.append($0) })
    pipeline.debugOptions.insert(.disablePrettyPrint)
    var pipelineActual = ""
    try! pipeline.format(
      syntax: sourceFileSyntax, source: originalSource, operatorTable: OperatorTable.standardOperators,
      assumingFileURL: nil, selection: .infinite, to: &pipelineActual)
    assertStringsEqualWithDiff(pipelineActual, expected)
    assertFindings(
      expected: findings, markerLocations: markedInput.markers,
      emittedFindings: emittedPipelineFindings, context: context, file: file, line: line)
  }
}

/// Returns a string containing a whitespace-insensitive representation of the given source file.
private func whitespaceInsensitiveText(of file: SourceFileSyntax) -> String {
  var result = ""
  for token in file.tokens(viewMode: .sourceAccurate) {
    appendNonspaceTrivia(token.leadingTrivia, to: &result)
    result.append(token.text)
    appendNonspaceTrivia(token.trailingTrivia, to: &result)
  }
  return result
}

/// Appends any non-whitespace trivia pieces from the given trivia collection to the output string.
private func appendNonspaceTrivia(_ trivia: Trivia, to string: inout String) {
  for piece in trivia {
    switch piece {
    case .carriageReturnLineFeeds, .carriageReturns, .formfeeds, .newlines, .spaces, .tabs:
      break
    case .lineComment(let comment), .docLineComment(let comment):
      // A tree transforming rule might leave whitespace at the end of a line comment, which the
      // pretty printer will remove, so we should ignore that.
      if let lastNonWhitespaceIndex = comment.lastIndex(where: { !$0.isWhitespace }) {
        string.append(contentsOf: comment[...lastNonWhitespaceIndex])
      } else {
        string.append(comment)
      }
    default:
      piece.write(to: &string)
    }
  }
}