File: MultiLineStringLiteralDiagnosticsGenerator.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 (169 lines) | stat: -rw-r--r-- 6,068 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
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2023 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
//
//===----------------------------------------------------------------------===//

#if swift(>=6)
internal import SwiftDiagnostics
@_spi(RawSyntax) internal import SwiftSyntax
#else
import SwiftDiagnostics
@_spi(RawSyntax) import SwiftSyntax
#endif

/// A diagnostic that `MultiLineStringLiteralIndentationDiagnosticsGenerator` is building.
/// As indentation errors are found on more lines, this diagnostic is modified
/// to include more fixIts.
private struct InProgressDiagnostic {
  let anchor: TokenSyntax
  let position: AbsolutePosition
  let kind: InvalidIndentationInMultiLineStringLiteralError.Kind
  var lines: Int
  var changes: [FixIt.Change]
  var handledNodes: [SyntaxIdentifier]
}

final class MultiLineStringLiteralIndentationDiagnosticsGenerator: SyntaxVisitor {

  // MARK: Entry

  public static func diagnose(
    _ node: StringLiteralExprSyntax
  ) -> [(diagnostic: Diagnostic, handledNodes: [SyntaxIdentifier])] {
    let visitor = MultiLineStringLiteralIndentationDiagnosticsGenerator(closeQuote: node.closingQuote)
    visitor.walk(node)
    visitor.finishInProgressDiagnostic()
    return visitor.finishedDiagnostics
  }

  // MARK: Implementation

  private let closeQuote: TokenSyntax

  /// Diagnostics that we have finished because their incorrect indentation was followed by correct indentation
  private var finishedDiagnostics: [(diagnostic: Diagnostic, handledNodes: [SyntaxIdentifier])] = []

  /// The diagnostic we are currently building up
  private var inProgressDiagnostic: InProgressDiagnostic?

  private init(closeQuote: TokenSyntax) {
    self.closeQuote = closeQuote
    super.init(viewMode: .sourceAccurate)
  }

  private func addIncorrectlyIndentedToken(token: TokenSyntax) {
    // Determine kind and position of the diagnostic
    var kind: InvalidIndentationInMultiLineStringLiteralError.Kind = .insufficientIndentation
    var position = token.positionAfterSkippingLeadingTrivia

    let tokenLeadingTrivia = token.leadingTrivia

    let indentationStartIndex =
      tokenLeadingTrivia.pieces.lastIndex(where: { $0.isNewline })?.advanced(by: 1) ?? tokenLeadingTrivia.startIndex
    let preIndentationTrivia = Trivia(pieces: tokenLeadingTrivia[0..<indentationStartIndex])
    let indentationTrivia = Trivia(pieces: tokenLeadingTrivia[indentationStartIndex...])
    var positionOffset = preIndentationTrivia.sourceLength.utf8Length

    for (invalidTriviaPiece, missingTriviaPiece) in zip(
      indentationTrivia.decomposed,
      closeQuote.leadingTrivia.decomposed
    ) {
      if invalidTriviaPiece == missingTriviaPiece {
        positionOffset += invalidTriviaPiece.sourceLength.utf8Length
        continue
      }
      switch invalidTriviaPiece {
      case .tabs: kind = .unexpectedTab
      case .spaces: kind = .unexpectedSpace
      default: break
      }
      position = token.position.advanced(by: positionOffset)
      break
    }

    // If the diagnostic we are currently building has a different kind, we
    // cannot merge them. Commit the current diagnostic so we can create a new one.
    if inProgressDiagnostic?.kind != kind {
      finishInProgressDiagnostic()
    }

    // Append the inProgressDiagnostic or create a new one.
    let changes = [
      FixIt.Change.replaceLeadingTrivia(token: token, newTrivia: preIndentationTrivia + closeQuote.leadingTrivia)
    ]
    let handledNodes = [token.id]
    if self.inProgressDiagnostic != nil {
      self.inProgressDiagnostic!.lines += 1
      self.inProgressDiagnostic!.changes += changes
      self.inProgressDiagnostic!.handledNodes += handledNodes
    } else {
      self.inProgressDiagnostic = InProgressDiagnostic(
        anchor: token,
        position: position,
        kind: kind,
        lines: 1,
        changes: changes,
        handledNodes: handledNodes
      )
    }
  }

  // Finish the diagnostic that's currently in progress so any new indentation
  // issue that's found will start a new diagnostic.
  private func finishInProgressDiagnostic() {
    guard let currentDiagnostic = self.inProgressDiagnostic else {
      return
    }

    let diagnostic = Diagnostic(
      node: Syntax(currentDiagnostic.anchor),
      position: currentDiagnostic.position,
      message: InvalidIndentationInMultiLineStringLiteralError(
        kind: currentDiagnostic.kind,
        lines: currentDiagnostic.lines
      ),
      highlights: [],
      notes: [Note(node: Syntax(closeQuote), message: .shouldMatchIndentationOfClosingQuote)],
      fixIts: [FixIt(message: .changeIndentationToMatchClosingDelimiter, changes: currentDiagnostic.changes)]
    )

    finishedDiagnostics.append((diagnostic, currentDiagnostic.handledNodes))
    self.inProgressDiagnostic = nil
  }

  private func isOnNewline(_ token: TokenSyntax) -> Bool {
    if token.leadingTrivia.contains(where: { $0.isNewline }) {
      return true
    }
    guard let previousToken = token.previousToken(viewMode: .sourceAccurate) else {
      return false
    }
    switch previousToken.tokenKind {
    case .stringSegment(let stringSegment):
      return stringSegment.last?.isNewline ?? false
    default:
      return previousToken.trailingTrivia.contains(where: { $0.isNewline })
    }
  }

  override func visit(_ token: TokenSyntax) -> SyntaxVisitorContinueKind {
    guard isOnNewline(token) else {
      // We are only interested in tokens at the start of a line
      return .visitChildren
    }

    if token.tokenDiagnostic?.kind == .insufficientIndentationInMultilineStringLiteral {
      addIncorrectlyIndentedToken(token: token)
    } else {
      finishInProgressDiagnostic()
    }
    return .visitChildren
  }
}